Skip to content

Commit ec8e956

Browse files
committed
[analyzer] NFC: Add a convenient CallDescriptionMap class.
It encapsulates the procedure of figuring out whether a call event corresponds to a function that's modeled by a checker. Checker developers no longer need to worry about performance of lookups into their own custom maps. Add unittests - which finally test CallDescription itself as well. Differential Revision: https://reviews.llvm.org/D62441 llvm-svn: 364866
1 parent 02f91dd commit ec8e956

File tree

5 files changed

+231
-41
lines changed

5 files changed

+231
-41
lines changed

clang/include/clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h

Lines changed: 65 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -71,39 +71,7 @@ enum CallEventKind {
7171
};
7272

7373
class CallEvent;
74-
75-
/// This class represents a description of a function call using the number of
76-
/// arguments and the name of the function.
77-
class CallDescription {
78-
friend CallEvent;
79-
80-
mutable IdentifierInfo *II = nullptr;
81-
mutable bool IsLookupDone = false;
82-
// The list of the qualified names used to identify the specified CallEvent,
83-
// e.g. "{a, b}" represent the qualified names, like "a::b".
84-
std::vector<const char *> QualifiedName;
85-
unsigned RequiredArgs;
86-
87-
public:
88-
const static unsigned NoArgRequirement = std::numeric_limits<unsigned>::max();
89-
90-
/// Constructs a CallDescription object.
91-
///
92-
/// @param QualifiedName The list of the name qualifiers of the function that
93-
/// will be matched. The user is allowed to skip any of the qualifiers.
94-
/// For example, {"std", "basic_string", "c_str"} would match both
95-
/// std::basic_string<...>::c_str() and std::__1::basic_string<...>::c_str().
96-
///
97-
/// @param RequiredArgs The number of arguments that is expected to match a
98-
/// call. Omit this parameter to match every occurrence of call with a given
99-
/// name regardless the number of arguments.
100-
CallDescription(ArrayRef<const char *> QualifiedName,
101-
unsigned RequiredArgs = NoArgRequirement)
102-
: QualifiedName(QualifiedName), RequiredArgs(RequiredArgs) {}
103-
104-
/// Get the name of the function that this object matches.
105-
StringRef getFunctionName() const { return QualifiedName.back(); }
106-
};
74+
class CallDescription;
10775

10876
template<typename T = CallEvent>
10977
class CallEventRef : public IntrusiveRefCntPtr<const T> {
@@ -1076,6 +1044,70 @@ class ObjCMethodCall : public CallEvent {
10761044
}
10771045
};
10781046

1047+
/// This class represents a description of a function call using the number of
1048+
/// arguments and the name of the function.
1049+
class CallDescription {
1050+
friend CallEvent;
1051+
1052+
mutable IdentifierInfo *II = nullptr;
1053+
mutable bool IsLookupDone = false;
1054+
// The list of the qualified names used to identify the specified CallEvent,
1055+
// e.g. "{a, b}" represent the qualified names, like "a::b".
1056+
std::vector<const char *> QualifiedName;
1057+
Optional<unsigned> RequiredArgs;
1058+
1059+
public:
1060+
/// Constructs a CallDescription object.
1061+
///
1062+
/// @param QualifiedName The list of the name qualifiers of the function that
1063+
/// will be matched. The user is allowed to skip any of the qualifiers.
1064+
/// For example, {"std", "basic_string", "c_str"} would match both
1065+
/// std::basic_string<...>::c_str() and std::__1::basic_string<...>::c_str().
1066+
///
1067+
/// @param RequiredArgs The number of arguments that is expected to match a
1068+
/// call. Omit this parameter to match every occurrence of call with a given
1069+
/// name regardless the number of arguments.
1070+
CallDescription(ArrayRef<const char *> QualifiedName,
1071+
Optional<unsigned> RequiredArgs = None)
1072+
: QualifiedName(QualifiedName), RequiredArgs(RequiredArgs) {}
1073+
1074+
/// Get the name of the function that this object matches.
1075+
StringRef getFunctionName() const { return QualifiedName.back(); }
1076+
};
1077+
1078+
/// An immutable map from CallDescriptions to arbitrary data. Provides a unified
1079+
/// way for checkers to react on function calls.
1080+
template <typename T> class CallDescriptionMap {
1081+
// Some call descriptions aren't easily hashable (eg., the ones with qualified
1082+
// names in which some sections are omitted), so let's put them
1083+
// in a simple vector and use linear lookup.
1084+
// TODO: Implement an actual map for fast lookup for "hashable" call
1085+
// descriptions (eg., the ones for C functions that just match the name).
1086+
std::vector<std::pair<CallDescription, T>> LinearMap;
1087+
1088+
public:
1089+
CallDescriptionMap(
1090+
std::initializer_list<std::pair<CallDescription, T>> &&List)
1091+
: LinearMap(List) {}
1092+
1093+
~CallDescriptionMap() = default;
1094+
1095+
// These maps are usually stored once per checker, so let's make sure
1096+
// we don't do redundant copies.
1097+
CallDescriptionMap(const CallDescriptionMap &) = delete;
1098+
CallDescriptionMap &operator=(const CallDescription &) = delete;
1099+
1100+
const T *lookup(const CallEvent &Call) const {
1101+
// Slow path: linear lookup.
1102+
// TODO: Implement some sort of fast path.
1103+
for (const std::pair<CallDescription, T> &I : LinearMap)
1104+
if (Call.isCalled(I.first))
1105+
return &I.second;
1106+
1107+
return nullptr;
1108+
}
1109+
};
1110+
10791111
/// Manages the lifetime of CallEvent objects.
10801112
///
10811113
/// CallEventManager provides a way to create arbitrary CallEvents "on the

clang/lib/StaticAnalyzer/Core/CallEvent.cpp

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -393,8 +393,7 @@ bool CallEvent::isCalled(const CallDescription &CD) const {
393393
return false;
394394
}
395395

396-
return (CD.RequiredArgs == CallDescription::NoArgRequirement ||
397-
CD.RequiredArgs == getNumArgs());
396+
return (!CD.RequiredArgs || CD.RequiredArgs == getNumArgs());
398397
}
399398

400399
SVal CallEvent::getArgSVal(unsigned Index) const {

clang/unittests/StaticAnalyzer/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ set(LLVM_LINK_COMPONENTS
44

55
add_clang_unittest(StaticAnalysisTests
66
AnalyzerOptionsTest.cpp
7+
CallDescriptionTest.cpp
78
StoreTest.cpp
89
RegisterCustomCheckersTest.cpp
910
SymbolReaperTest.cpp
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
//===- unittests/StaticAnalyzer/CallDescriptionTest.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 "Reusables.h"
10+
11+
#include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h"
12+
#include "clang/Tooling/Tooling.h"
13+
#include "gtest/gtest.h"
14+
15+
namespace clang {
16+
namespace ento {
17+
namespace {
18+
19+
// A wrapper around CallDescriptionMap<bool> that allows verifying that
20+
// all functions have been found. This is needed because CallDescriptionMap
21+
// isn't supposed to support iteration.
22+
class ResultMap {
23+
size_t Found, Total;
24+
CallDescriptionMap<bool> Impl;
25+
26+
public:
27+
ResultMap(std::initializer_list<std::pair<CallDescription, bool>> Data)
28+
: Found(0),
29+
Total(std::count_if(Data.begin(), Data.end(),
30+
[](const std::pair<CallDescription, bool> &Pair) {
31+
return Pair.second == true;
32+
})),
33+
Impl(std::move(Data)) {}
34+
35+
const bool *lookup(const CallEvent &Call) {
36+
const bool *Result = Impl.lookup(Call);
37+
// If it's a function we expected to find, remember that we've found it.
38+
if (Result && *Result)
39+
++Found;
40+
return Result;
41+
}
42+
43+
// Fail the test if we haven't found all the true-calls we were looking for.
44+
~ResultMap() { EXPECT_EQ(Found, Total); }
45+
};
46+
47+
// Scan the code body for call expressions and see if we find all calls that
48+
// we were supposed to find ("true" in the provided ResultMap) and that we
49+
// don't find the ones that we weren't supposed to find
50+
// ("false" in the ResultMap).
51+
class CallDescriptionConsumer : public ExprEngineConsumer {
52+
ResultMap &RM;
53+
void performTest(const Decl *D) {
54+
using namespace ast_matchers;
55+
56+
if (!D->hasBody())
57+
return;
58+
59+
const CallExpr *CE = findNode<CallExpr>(D, callExpr());
60+
const StackFrameContext *SFC =
61+
Eng.getAnalysisDeclContextManager().getStackFrame(D);
62+
ProgramStateRef State = Eng.getInitialState(SFC);
63+
CallEventRef<> Call =
64+
Eng.getStateManager().getCallEventManager().getCall(CE, State, SFC);
65+
66+
const bool *LookupResult = RM.lookup(*Call);
67+
// Check that we've found the function in the map
68+
// with the correct description.
69+
EXPECT_TRUE(LookupResult && *LookupResult);
70+
71+
// ResultMap is responsible for making sure that we've found *all* calls.
72+
}
73+
74+
public:
75+
CallDescriptionConsumer(CompilerInstance &C,
76+
ResultMap &RM)
77+
: ExprEngineConsumer(C), RM(RM) {}
78+
79+
bool HandleTopLevelDecl(DeclGroupRef DG) override {
80+
for (const auto *D : DG)
81+
performTest(D);
82+
return true;
83+
}
84+
};
85+
86+
class CallDescriptionAction : public ASTFrontendAction {
87+
ResultMap RM;
88+
89+
public:
90+
CallDescriptionAction(
91+
std::initializer_list<std::pair<CallDescription, bool>> Data)
92+
: RM(Data) {}
93+
94+
std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &Compiler,
95+
StringRef File) override {
96+
return llvm::make_unique<CallDescriptionConsumer>(Compiler, RM);
97+
}
98+
};
99+
100+
TEST(CallEvent, CallDescription) {
101+
// Test simple name matching.
102+
EXPECT_TRUE(tooling::runToolOnCode(
103+
new CallDescriptionAction({
104+
{{"bar"}, false}, // false: there's no call to 'bar' in this code.
105+
{{"foo"}, true}, // true: there's a call to 'foo' in this code.
106+
}), "void foo(); void bar() { foo(); }"));
107+
108+
// Test arguments check.
109+
EXPECT_TRUE(tooling::runToolOnCode(
110+
new CallDescriptionAction({
111+
{{"foo", 1}, true},
112+
{{"foo", 2}, false},
113+
}), "void foo(int); void foo(int, int); void bar() { foo(1); }"));
114+
115+
// Test lack of arguments check.
116+
EXPECT_TRUE(tooling::runToolOnCode(
117+
new CallDescriptionAction({
118+
{{"foo", None}, true},
119+
{{"foo", 2}, false},
120+
}), "void foo(int); void foo(int, int); void bar() { foo(1); }"));
121+
122+
// Test qualified names.
123+
EXPECT_TRUE(tooling::runToolOnCode(
124+
new CallDescriptionAction({
125+
{{{"std", "basic_string", "c_str"}}, true},
126+
}),
127+
"namespace std { inline namespace __1 {"
128+
" template<typename T> class basic_string {"
129+
" public:"
130+
" T *c_str();"
131+
" };"
132+
"}}"
133+
"void foo() {"
134+
" using namespace std;"
135+
" basic_string<char> s;"
136+
" s.c_str();"
137+
"}"));
138+
139+
// A negative test for qualified names.
140+
EXPECT_TRUE(tooling::runToolOnCode(
141+
new CallDescriptionAction({
142+
{{{"foo", "bar"}}, false},
143+
{{{"bar", "foo"}}, false},
144+
{{"foo"}, true},
145+
}), "void foo(); struct bar { void foo(); }; void test() { foo(); }"));
146+
}
147+
148+
} // namespace
149+
} // namespace ento
150+
} // namespace clang

clang/unittests/StaticAnalyzer/Reusables.h

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,24 @@
1717
namespace clang {
1818
namespace ento {
1919

20+
// Find a node in the current AST that matches a matcher.
21+
template <typename T, typename MatcherT>
22+
const T *findNode(const Decl *Where, MatcherT What) {
23+
using namespace ast_matchers;
24+
auto Matches = match(decl(hasDescendant(What.bind("root"))),
25+
*Where, Where->getASTContext());
26+
assert(Matches.size() <= 1 && "Ambiguous match!");
27+
assert(Matches.size() >= 1 && "Match not found!");
28+
const T *Node = selectFirst<T>("root", Matches);
29+
assert(Node && "Type mismatch!");
30+
return Node;
31+
}
32+
2033
// Find a declaration in the current AST by name.
2134
template <typename T>
2235
const T *findDeclByName(const Decl *Where, StringRef Name) {
2336
using namespace ast_matchers;
24-
auto Matcher = decl(hasDescendant(namedDecl(hasName(Name)).bind("d")));
25-
auto Matches = match(Matcher, *Where, Where->getASTContext());
26-
assert(Matches.size() == 1 && "Ambiguous name!");
27-
const T *Node = selectFirst<T>("d", Matches);
28-
assert(Node && "Name not found!");
29-
return Node;
37+
return findNode<T>(Where, namedDecl(hasName(Name)));
3038
}
3139

3240
// A re-usable consumer that constructs ExprEngine out of CompilerInvocation.

0 commit comments

Comments
 (0)