Skip to content

Commit c281dbd

Browse files
Merge pull request #1169 from castler/fix-rule-0-2-4-unused-limited-visibility-fp
Fix RULE-0-2-4/A0-1-3 false positives for private functions
2 parents aaac22d + ee2ef75 commit c281dbd

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)