Skip to content

Commit ee2ef75

Browse files
committed
Fix RULE-0-2-4/A0-1-3 false positives for private functions
Two categories of false positive are addressed: 1. **Private pure virtual functions used via the NVI idiom** — `DynamicCallGraph::getTarget()` resolves a virtual call to the implementations that may actually *run*. A pure virtual function (`= 0`) has no body, so it is never a viable dispatch target and is therefore never returned by `getTarget()` — not even when it is unambiguously named by a call, as in the non-virtual interface (NVI) idiom where a public member calls a private pure virtual. It was consequently reported as unused even though a sibling member calls it. `functionIsCalled` now also counts the *statically named* callee (`FunctionCall.getTarget()`), which recovers exactly that case. This is deliberately narrower than excluding `PureVirtualFunction` from `LocalFunction` outright: a private pure virtual that is genuinely never called and never overridden is still reported. 2. **Private members of never-instantiated class templates** — when a class template is never instantiated with a concrete type anywhere in the analyzed compilation units, Clang never elaborates a body for its member functions, so `Call`/`FunctionCall` targets within that pattern's own text cannot be resolved by `DynamicCallGraph::getTarget()` or `VirtualDispatch`, even for calls between sibling members of the very same class (e.g. a public entry point calling a private helper). This is common for generic "plumbing" library code (CRTP-style wrappers and the like) that is only ever instantiated by downstream consumers outside the analyzed codebase. The new `hasNoVisibleInstantiation(fn)` predicate conservatively treats such private members as "used" (out of scope for this analysis) rather than reporting them as dead code — but only when *no* sibling member of the same class has any instantiation either, so genuinely dead private helpers in class templates that *are* instantiated elsewhere are still correctly reported. The sibling-instantiation check is factored into a `pragma[noinline]` predicate over the declaring `Class` rather than over the member `Function`. Inlined into the caller, the join orderer loses the fact that the class is functionally determined and materialises the full (member, sibling) cross product per class before projecting it away. On a large real-world database this cut the peak intermediate relation for that predicate from 4,831,818 tuples to 7,700 with identical results. Fixes #1168
1 parent 62bf905 commit ee2ef75

4 files changed

Lines changed: 175 additions & 3 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
- `A0-1-3`, `RULE-0-2-4` - `UnusedLocalFunction.ql`:
2+
- Fixed false positives for private pure virtual functions used through the non-virtual
3+
interface (NVI) idiom. `DynamicCallGraph::getTarget()` resolves a virtual call to the
4+
implementations that may actually run; a pure virtual function has no body, so it is
5+
never a viable dispatch target and was reported as unused even when a sibling member
6+
called it. A call is now also counted when the function is the statically named
7+
callee. Pure virtual functions that are genuinely never called and never overridden
8+
are still reported.
9+
- Excluded private member functions of class templates that are never concretely
10+
instantiated anywhere in the database (and where no sibling member of the same
11+
class-template pattern is instantiated either). Clang never elaborates a body for the
12+
members of such patterns, so calls between sibling members of the same
13+
never-instantiated class (e.g. a public entry point calling a private helper) cannot be
14+
resolved by the call graph. This is common for generic "plumbing" library code
15+
(CRTP-style wrappers, etc.) that is only ever
16+
instantiated by downstream consumers outside of the analyzed codebase.

cpp/common/src/codingstandards/cpp/rules/unusedlocalfunction/UnusedLocalFunction.qll

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,84 @@ predicate overloadedFunctionIsCalled(Function unusedFunction) {
2020
exists(Function f | f = unusedFunction.getAnOverload() and f = getTarget(_))
2121
}
2222

23+
/**
24+
* Holds if `fn` is the target of some call, either statically or according to the
25+
* dynamic call graph.
26+
*
27+
* `DynamicCallGraph::getTarget()` resolves a virtual call to the functions that may
28+
* actually run, i.e. the overriding implementations. A pure virtual function has no
29+
* body, so it is never a viable dispatch target and is therefore *never* returned by
30+
* `getTarget()` -- even when it is unambiguously named by a call, as in the
31+
* non-virtual interface (NVI) idiom where a public member calls a private pure
32+
* virtual. The additional static `FunctionCall.getTarget()` disjunct recovers exactly
33+
* that case: the callee as written in the source.
34+
*/
35+
predicate functionIsCalled(Function fn) {
36+
fn = getTarget(_)
37+
or
38+
// The statically named callee, which the dynamic call graph drops for calls that
39+
// dispatch to an override (notably pure virtual functions, which have no body).
40+
exists(FunctionCall fc | fc.getTarget() = fn)
41+
}
42+
2343
/** Checks if a Function's address was taken. */
2444
predicate addressBeenTaken(Function unusedFunction) {
2545
exists(FunctionAccess fa | fa.getTarget() = unusedFunction)
2646
}
2747

48+
/**
49+
* Holds if some member of the class `c` has at least one concrete instantiation anywhere in the
50+
* database.
51+
*
52+
* If this holds for the declaring type of a member function `fn`, the class template is genuinely
53+
* "alive" (used with a concrete type somewhere), and the fact that `fn` itself was never
54+
* instantiated is real evidence that it is unused: for a member function to lack a concrete
55+
* instantiation while sibling members do have one, it must never have been called from any of
56+
* those sibling bodies.
57+
*
58+
* `pragma[noinline]` keeps this a standalone relation of arity one. Inlined into the caller, the
59+
* join orderer loses the fact that `c` is functionally determined and materialises the full
60+
* (member, sibling) cross product per class before projecting it away, which is quadratic in the
61+
* size of the largest class.
62+
*/
63+
pragma[noinline]
64+
private predicate classHasAnyInstantiatedMember(Class c) {
65+
exists(Function sibling, Function siblingInstantiation |
66+
sibling.getDeclaringType() = c and
67+
siblingInstantiation.isConstructedFrom(sibling)
68+
)
69+
}
70+
71+
/**
72+
* Holds if `fn` is a function from an uninstantiated template for which no concrete
73+
* instantiation exists anywhere in the database, and no other member of the same
74+
* class-template pattern is instantiated either.
75+
*
76+
* When a class template is never instantiated with a concrete type in the analyzed
77+
* compilation units, Clang never elaborates a body for its member functions, so
78+
* `Call`/`FunctionCall` targets within that pattern's own text cannot be resolved by
79+
* `DynamicCallGraph::getTarget()` or `VirtualDispatch`, even for calls between sibling members
80+
* of the very same class (e.g. a constructor calling a private helper). This is common for
81+
* generic "plumbing" library code (CRTP-style wrappers, etc.)
82+
* that is only ever instantiated by downstream consumers outside of this codebase. In that
83+
* situation we have no visibility at all into the call graph, so we conservatively treat the
84+
* function as "used" (out of scope for this analysis) rather than report it as dead code.
85+
*
86+
* We only do this when *no* sibling member of the class pattern has an instantiation either
87+
* (see `classHasAnyInstantiatedMember`): if some sibling *is* instantiated, the class is
88+
* genuinely used, and `fn` lacking an instantiation is real (not merely missing) evidence that
89+
* it is unused.
90+
*/
91+
predicate hasNoVisibleInstantiation(Function fn) {
92+
// Restricted to class-template members: a standalone function template that is never
93+
// instantiated anywhere is genuinely dead code, and detecting that does not suffer from the
94+
// "sibling member of the same class" ambiguity this predicate is designed for.
95+
fn instanceof MemberFunction and
96+
fn.isFromUninstantiatedTemplate(_) and
97+
not exists(Function instantiation | instantiation.isConstructedFrom(fn)) and
98+
not classHasAnyInstantiatedMember(fn.getDeclaringType())
99+
}
100+
28101
/** A `Function` nested in an anonymous namespace. */
29102
class AnonymousNamespaceFunction extends Function {
30103
AnonymousNamespaceFunction() { getNamespace().getParentNamespace*().isAnonymous() }
@@ -74,7 +147,7 @@ module UnusedLocalFunction<UnusedLocalFunctionConfigSig Config> {
74147
query predicate problems(LocalFunction unusedLocalFunction, string message) {
75148
not isExcluded(unusedLocalFunction, Config::getQuery()) and
76149
// No static or dynamic call target for this function
77-
not unusedLocalFunction = getTarget(_) and
150+
not functionIsCalled(unusedLocalFunction) and
78151
// If this is a TemplateFunction or an instantiation of a template, then only report it as unused
79152
// if all other instantiations of the template are unused
80153
not exists(
@@ -88,7 +161,7 @@ module UnusedLocalFunction<UnusedLocalFunctionConfigSig Config> {
88161
|
89162
// There exists an instantiation which is called
90163
functionFromInstantiatedTemplate.isConstructedFrom(functionFromUninstantiatedTemplate) and
91-
functionFromInstantiatedTemplate = getTarget(_)
164+
functionIsCalled(functionFromInstantiatedTemplate)
92165
) and
93166
// A function is defined as "used" if any one of the following holds true:
94167
// - It's an explicitly deleted functions e.g. =delete
@@ -100,6 +173,9 @@ module UnusedLocalFunction<UnusedLocalFunctionConfigSig Config> {
100173
not unusedLocalFunction.getAnAttribute().getName() = "maybe_unused" and
101174
not overloadedFunctionIsCalled(unusedLocalFunction) and
102175
not addressBeenTaken(unusedLocalFunction) and
176+
// We have no visibility into the call graph of a template that is never instantiated
177+
// anywhere in the database, so we cannot reliably tell it is unused.
178+
not hasNoVisibleInstantiation(unusedLocalFunction) and
103179
message =
104180
unusedLocalFunction.getLocalFunctionType() + " function " + unusedLocalFunction.getName() +
105181
" is not statically called, or is in an unused template."

cpp/common/test/rules/unusedlocalfunction/UnusedLocalFunction.expected

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@
66
| test.cpp:85:6:85:7 | h3 | Anonymous namespace function h3 is not statically called, or is in an unused template. |
77
| test.cpp:144:8:144:8 | f | Anonymous namespace class member function f is not statically called, or is in an unused template. |
88
| test.cpp:150:8:150:8 | f | Anonymous namespace class member function f is not statically called, or is in an unused template. |
9+
| test.cpp:214:9:214:18 | deadHelper | Private member function deadHelper is not statically called, or is in an unused template. |
10+
| test.cpp:236:16:236:28 | neverUsedPure | Private member function neverUsedPure is not statically called, or is in an unused template. |

cpp/common/test/rules/unusedlocalfunction/test.cpp

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,4 +157,82 @@ class C3 {
157157
public:
158158
void f() {} // COMPLIANT - public external linkage
159159
};
160-
} // namespace N1
160+
} // namespace N1
161+
162+
class PureVirtualBase {
163+
public:
164+
void callImpl() { impl(); }
165+
166+
private:
167+
virtual void impl() = 0; // COMPLIANT - pure virtual contract.
168+
};
169+
170+
class PureVirtualDerived : public PureVirtualBase {
171+
private:
172+
void impl() override {}
173+
};
174+
175+
void test_pure_virtual_private_member() {
176+
PureVirtualDerived derived;
177+
derived.callImpl();
178+
}
179+
180+
/**
181+
* Class templates that are never instantiated anywhere in the analyzed
182+
* compilation units.
183+
*
184+
* Clang never elaborates a body for the members of such patterns, so calls
185+
* between sibling members (even genuine ones, like a public entry point calling
186+
* a private helper) cannot be resolved by the call graph. We conservatively
187+
* treat all of them as used, rather than risk reporting them as dead code.
188+
*/
189+
template <class NeverUsedT> class NeverInstantiatedFactory {
190+
public:
191+
static void Create() { instanceHelper(); }
192+
193+
private:
194+
static void instanceHelper() {
195+
} // COMPLIANT - class template is never instantiated anywhere in this
196+
// translation unit, so the analysis has no visibility into whether
197+
// `Create` (also never instantiated) really calls it; conservatively
198+
// not reported.
199+
};
200+
201+
/**
202+
* A class template that *is* instantiated (and its caller genuinely used), so
203+
* the ordinary per-instantiation call-graph reasoning applies and a
204+
* truly-unused private helper is still correctly reported.
205+
*/
206+
template <class UsedT> class InstantiatedFactory {
207+
public:
208+
UsedT get() { return makeValue(); }
209+
210+
private:
211+
UsedT makeValue() {
212+
return UsedT();
213+
} // COMPLIANT - called by get(), which is instantiated.
214+
UsedT deadHelper() { // NON_COMPLIANT - never called, and the class template
215+
// is instantiated, so the analysis does have visibility
216+
// into this member.
217+
return UsedT();
218+
}
219+
};
220+
221+
void test_instantiated_factory() {
222+
InstantiatedFactory<int> factory;
223+
factory.get();
224+
}
225+
/**
226+
* A private pure virtual that is genuinely dead: it is never called through the
227+
* non-virtual interface, and no derived class ever overrides it. Pure virtual
228+
* functions are deliberately in scope for this query (see
229+
* `UnusedFunctions::UsableFunction`), so this must still be reported.
230+
*/
231+
class DeadPureVirtualBase {
232+
public:
233+
void unrelated() {}
234+
235+
private:
236+
virtual void neverUsedPure() = 0; // NON_COMPLIANT - never called, never
237+
// overridden.
238+
};

0 commit comments

Comments
 (0)