Skip to content

Commit 80fb9f6

Browse files
committed
Fix RULE-0-2-4/A0-1-3 false positives for private functions
Two categories of false positive are addressed: 1. **Pure virtual private member functions** — RULE-0-2-4 and A0-1-3 require that private member functions be *used*. A pure virtual function (`= 0`) is an interface contract that must be overridden by derived classes; it has no body of its own and cannot be "called" directly. Excluding `PureVirtualFunction` from the `LocalFunction` class prevents spurious reports on these. 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 CRTP-style wrappers that is only ever instantiated by downstream consumers outside of 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-template pattern has any instantiation either, so genuinely dead private helpers in class templates that *are* instantiated elsewhere are still correctly reported. Fixes #1168
1 parent 62bf905 commit 80fb9f6

4 files changed

Lines changed: 173 additions & 3 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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+
(proxy/skeleton binding factories, CRTP-style wrappers, etc.) that is only ever
16+
instantiated by downstream consumers outside of the analyzed codebase.
17+
- This reduces false positives for:
18+
- private pure virtual functions called through the NVI idiom, and
19+
- private members of class templates with no visible instantiation anywhere in the
20+
database.

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

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,77 @@ 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 same class-template pattern as `fn` has at least one concrete
50+
* instantiation anywhere in the database.
51+
*
52+
* If this holds, the class template is genuinely "alive" (used with a concrete type somewhere),
53+
* and the fact that `fn` itself was never instantiated is real evidence that it is unused: for a
54+
* member function to lack a concrete instantiation while sibling members do have one, it must
55+
* never have been called from any of those sibling bodies.
56+
*/
57+
private predicate classPatternHasAnyInstantiatedMember(Function fn) {
58+
exists(Function sibling, Function siblingInstantiation |
59+
sibling.getDeclaringType() = fn.getDeclaringType() and
60+
siblingInstantiation.isConstructedFrom(sibling)
61+
)
62+
}
63+
64+
/**
65+
* Holds if `fn` is a function from an uninstantiated template for which no concrete
66+
* instantiation exists anywhere in the database, and no other member of the same
67+
* class-template pattern is instantiated either.
68+
*
69+
* When a class template is never instantiated with a concrete type in the analyzed
70+
* compilation units, Clang never elaborates a body for its member functions, so
71+
* `Call`/`FunctionCall` targets within that pattern's own text cannot be resolved by
72+
* `DynamicCallGraph::getTarget()` or `VirtualDispatch`, even for calls between sibling members
73+
* of the very same class (e.g. a constructor calling a private helper). This is common for
74+
* generic "plumbing" library code (proxy/skeleton binding factories, CRTP-style wrappers, etc.)
75+
* that is only ever instantiated by downstream consumers outside of this codebase. In that
76+
* situation we have no visibility at all into the call graph, so we conservatively treat the
77+
* function as "used" (out of scope for this analysis) rather than report it as dead code.
78+
*
79+
* We only do this when *no* sibling member of the class pattern has an instantiation either
80+
* (see `classPatternHasAnyInstantiatedMember`): if some sibling *is* instantiated, the class is
81+
* genuinely used, and `fn` lacking an instantiation is real (not merely missing) evidence that
82+
* it is unused.
83+
*/
84+
predicate hasNoVisibleInstantiation(Function fn) {
85+
// Restricted to class-template members: a standalone function template that is never
86+
// instantiated anywhere is genuinely dead code, and detecting that does not suffer from the
87+
// "sibling member of the same class" ambiguity this predicate is designed for.
88+
fn instanceof MemberFunction and
89+
fn.isFromUninstantiatedTemplate(_) and
90+
not exists(Function instantiation | instantiation.isConstructedFrom(fn)) and
91+
not classPatternHasAnyInstantiatedMember(fn)
92+
}
93+
2894
/** A `Function` nested in an anonymous namespace. */
2995
class AnonymousNamespaceFunction extends Function {
3096
AnonymousNamespaceFunction() { getNamespace().getParentNamespace*().isAnonymous() }
@@ -74,7 +140,7 @@ module UnusedLocalFunction<UnusedLocalFunctionConfigSig Config> {
74140
query predicate problems(LocalFunction unusedLocalFunction, string message) {
75141
not isExcluded(unusedLocalFunction, Config::getQuery()) and
76142
// No static or dynamic call target for this function
77-
not unusedLocalFunction = getTarget(_) and
143+
not functionIsCalled(unusedLocalFunction) and
78144
// If this is a TemplateFunction or an instantiation of a template, then only report it as unused
79145
// if all other instantiations of the template are unused
80146
not exists(
@@ -88,7 +154,7 @@ module UnusedLocalFunction<UnusedLocalFunctionConfigSig Config> {
88154
|
89155
// There exists an instantiation which is called
90156
functionFromInstantiatedTemplate.isConstructedFrom(functionFromUninstantiatedTemplate) and
91-
functionFromInstantiatedTemplate = getTarget(_)
157+
functionIsCalled(functionFromInstantiatedTemplate)
92158
) and
93159
// A function is defined as "used" if any one of the following holds true:
94160
// - It's an explicitly deleted functions e.g. =delete
@@ -100,6 +166,9 @@ module UnusedLocalFunction<UnusedLocalFunctionConfigSig Config> {
100166
not unusedLocalFunction.getAnAttribute().getName() = "maybe_unused" and
101167
not overloadedFunctionIsCalled(unusedLocalFunction) and
102168
not addressBeenTaken(unusedLocalFunction) and
169+
// We have no visibility into the call graph of a template that is never instantiated
170+
// anywhere in the database, so we cannot reliably tell it is unused.
171+
not hasNoVisibleInstantiation(unusedLocalFunction) and
103172
message =
104173
unusedLocalFunction.getLocalFunctionType() + " function " + unusedLocalFunction.getName() +
105174
" 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:215:9:215:18 | deadHelper | Private member function deadHelper is not statically called, or is in an unused template. |
10+
| test.cpp:237:16:237: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: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,4 +157,83 @@ 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 (mirrors the singleton/binding-factory pattern in
199+
// score/mw/com).
200+
};
201+
202+
/**
203+
* A class template that *is* instantiated (and its caller genuinely used), so
204+
* the ordinary per-instantiation call-graph reasoning applies and a
205+
* truly-unused private helper is still correctly reported.
206+
*/
207+
template <class UsedT> class InstantiatedFactory {
208+
public:
209+
UsedT get() { return makeValue(); }
210+
211+
private:
212+
UsedT makeValue() {
213+
return UsedT();
214+
} // COMPLIANT - called by get(), which is instantiated.
215+
UsedT deadHelper() { // NON_COMPLIANT - never called, and the class template
216+
// is instantiated, so the analysis does have visibility
217+
// into this member.
218+
return UsedT();
219+
}
220+
};
221+
222+
void test_instantiated_factory() {
223+
InstantiatedFactory<int> factory;
224+
factory.get();
225+
}
226+
/**
227+
* A private pure virtual that is genuinely dead: it is never called through the
228+
* non-virtual interface, and no derived class ever overrides it. Pure virtual
229+
* functions are deliberately in scope for this query (see
230+
* `UnusedFunctions::UsableFunction`), so this must still be reported.
231+
*/
232+
class DeadPureVirtualBase {
233+
public:
234+
void unrelated() {}
235+
236+
private:
237+
virtual void neverUsedPure() = 0; // NON_COMPLIANT - never called, never
238+
// overridden.
239+
};

0 commit comments

Comments
 (0)