@@ -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. */
2444predicate 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. */
29102class 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."
0 commit comments