Skip to content

Commit 9c275c1

Browse files
committed
Python: Implement call-graph with type-trackers
This commit is a squash of 80 other commits. While developing, things changed majorly 2-3 times, and it just wasn't feasible to go back and write a really nice commit history. My apologies for this HUGE commit. Also, later on this is where I solved merge conflicts after flow-summaries PR was merged. For your amusement, I've included the original commit messages below. Python: Add proper argument/parameter positions Python: Handle normal function calls Python: Reduce dataflow-consistency warnings Previously there was a lot of failures for `uniqueEnclosingCallable` and `argHasPostUpdate` Removing the override of `getEnclosingCallable` in ParameterNode is probably the most controversial... although from my point of view it's a change for the better, since we're able to provide data-flow ParameterNodes for more of the AST parameter nodes. Python: Adjust `dataflow/calls` test Python: Implement `isParameterOf`/`argumentOf`/`OutNode` This makes the tests under `dataflow/basic` work as well 👍 (initially I had these as separate commits, but it felt like it was too much noise) Python: Accept fix for `dataflow/consistency` Python: Changes to `coverage/argumentRoutingTest.ql` Notice we gain a few new resolved arguments. We loose out on stuff due to: 1. not handling `*` or `**` in either arguments/parameters (yet) 2. not handling special calls (yet) Python: Small fix for `TestUtil/RoutingTest.qll` Since the helper predicates do not depend on this, moved outside class. Python: Accept changes to `dataflow/coverage/NormalDataflowTest.ql` Most of this is due to: - not handling any kinds of methods yet - not handling `*` or `**` Python: Small investigation of `test_deep_callgraph` Python: Accept changes to `coverage/localFlow.ql` I don't fully understand why the .expected file changed. Since we still have the desired flow, I'm not going to worry too much about it. with this commit, the `dataflow/coverage` tests passes 👍 Python: Minor doc update Python: Add staticmethod/classmethod to `dataflow/calls` Python: Handle method calls on class instances without trying to deal with any class inheritance, or staticmethod/classmethod at all. Notice that with this change, we only have a DataFlowCall for the calls that we can actually resolve. I'm not 100% sure if we need to add a `UnresolvedCall` subclass of `DataFlowCall` for MaD in the future, but it should be easy to do. I'm still unsure about the value of `classesCallGraph`, but have just accepted the changes. Python: Handle direct method calls `C.foo(C, arg0)` Python: Handle `@staticmethod` Python: Handle class method calls... but the code is shit WIP todo Rewrite method calls to be better also fixed a problem with `self` being an argument to the `x.staticmethod()` call :| Python: Add subclass tests Python: Split `class_advanced` test Python: Rewrite call-graph tests to be inline expectation (1/2) This adds inline expectations, next commit will remove old annotations code... but I thought it would be easier to review like this. Minor fixup Python: Add simple subclass support Python: more precise subclass lookup Still not 100% precise.. but it's better New ambiguous Python: Add test for `self.m()` and `cls.m()` calls Python: Handle `self.m()` and `cls.m()` calls Python: Add tests for `__init__` and `__new__` Python: Handle class calls Python: Fix `self` argument passing for class calls Now field-flow tests also pass 💪 (although the crosstalk fieldflow test changes were due to this specific commit) I also copied much of the setup for pre/post update nodes from Ruby, specifically having the abstract `PostUpdateNodeImpl` in DataFlowPrivate seemed like a nice change. Same for the setup with `TNode` definition having the specification directly in the body, instead of a `NeedsSyntheticPostUpdateNode` class. Python: Add new crosstalk test WIP Maybe needs a bit of refactoring, and to see how it all behaves with points-to Python: Add `super()` call-graph tests Python: Refactor MethodCall char-pred In anticipation of supporting `super(MyClass, self).foo()`, where the `self` argument doesn't come from an AttrNode, but from the second argument to super. Without `pragma[inline]` the optimizer found a terrible join-order -- this won't guarantee a good join-order for the future, but for now it was just so simple and could let me move on with life. Python: Add basic `super()` support I debated a little (with myself) whether I should really do `superTracker`, but I thought "why not" and just rolled with it. I did not confirm whether it was actually needed anywhere, that is if anyone does `ref = super; ref().foo()` -- although I certainly doubt it's very wide-spread. Python: InlineCallGraphTest: Allow non-unique callable name in different files Python: more MRO tests Python: Add MRO approximation for `super()` Although it's not 100% accurate, it seems to be on level with the one in points-to. Python: Remove some spurious targets for direct calls removal of TODO from refactoring remove TODOs class call support Python: Add contrived subclass call example Python: Remove more spurious call targets NOTE: I initially forgot to use `findFunctionAccordingToMroKnownStartingClass` instead of `findFunctionAccordingToMro` for __init__ and __new__, and since I did make that mistake myself, I wanted to add something to the test to highlight this fact, and make it viewable by PR reviewer... this will be fixed in the next commit. Python: Proper fix for spurious __init__ targets Python: Add call-graph example of class decorator Python: Support decorated classes in new call-graph Python: Add call-graph tests for `type(obj).meth()` Python: support `type(obj).meth()` Python: Add test for callable defined in function Python: Add test for callable as argument Current'y we don't find these with type-tracking, which is super mysterious. I did check that we have proper flow from the arguments to the parameters. Python: Found problem for callable as argument :| MAJOR WIP WIP commit IT WORKS AGAIN (but terrible performance) remove pragma[inline] remove oops Fix performance problem I tried to optimize it even further, but I didn't end up achieving anything :| Fix call-graph comparison add comparison version with easy lookup incomplete missing call-graph tests unhandled tests trying to replicate missing call-edge due to missing imports ... but it's hard also seems to be problems with the inline-expectation-value that I used, seems like it has both missing/unexpected results with same value Python: Add import-problem test Python: Add shadowing problem some cleanup of rewrite fix a little more cleanup Add consistency queries to call-graph tests Python: Add post-update nodes for `self` in implicit `super()` uses But we do need to discuss whether this is the right approach :O Fix for field-flow tests This came from more precise argument passing Fixed results in type-tracking Comes from better argument passing with super() and handling of functions with decorators fix of inline call graph tests Fixup call annotation test Many minor cleanups/fixes NewNormalCall -> NormalCall Python: Major restructuring + qldoc writing Python: Accept changes from pre/post update node .toString changes Python: Reduce `super` complexity !! WIP !! Python: Only pass self-reference if in same enclosing-callable Python: Add call-graph test with nested class This was inspired by the ImpliesDataflow test that showed missing flow for q_super, but at least for the call-graph, I'm not able to reproduce this missing result :| Python: Restrict `super()` to function defined directly on class Python: Accept fixes to ImpliesDataflow Python: Expand field-flow crosstalk tests
1 parent aa78a43 commit 9c275c1

File tree

81 files changed

+2483
-436
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

81 files changed

+2483
-436
lines changed

python/ql/lib/semmle/python/dataflow/new/internal/DataFlowDispatch.qll

Lines changed: 902 additions & 13 deletions
Large diffs are not rendered by default.

python/ql/lib/semmle/python/dataflow/new/internal/DataFlowPrivate.qll

Lines changed: 24 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -39,158 +39,48 @@ predicate isArgumentNode(ArgumentNode arg, DataFlowCall c, ArgumentPosition pos)
3939
//--------
4040
predicate isExpressionNode(ControlFlowNode node) { node.getNode() instanceof Expr }
4141

42-
/** DEPRECATED: Alias for `SyntheticPreUpdateNode` */
43-
deprecated module syntheticPreUpdateNode = SyntheticPreUpdateNode;
42+
class SyntheticPreUpdateNode extends Node, TSyntheticPreUpdateNode {
43+
CallNode node;
4444

45-
/** A module collecting the different reasons for synthesising a pre-update node. */
46-
module SyntheticPreUpdateNode {
47-
class SyntheticPreUpdateNode extends Node, TSyntheticPreUpdateNode {
48-
NeedsSyntheticPreUpdateNode post;
45+
SyntheticPreUpdateNode() { this = TSyntheticPreUpdateNode(node) }
4946

50-
SyntheticPreUpdateNode() { this = TSyntheticPreUpdateNode(post) }
47+
/** Gets the node for which this is a synthetic pre-update node. */
48+
CfgNode getPostUpdateNode() { result.getNode() = node }
5149

52-
/** Gets the node for which this is a synthetic pre-update node. */
53-
Node getPostUpdateNode() { result = post }
50+
override string toString() { result = "[pre] " + node.toString() }
5451

55-
override string toString() { result = "[pre " + post.label() + "] " + post.toString() }
52+
override Scope getScope() { result = node.getScope() }
5653

57-
override Scope getScope() { result = post.getScope() }
58-
59-
override Location getLocation() { result = post.getLocation() }
60-
}
61-
62-
/** A data flow node for which we should synthesise an associated pre-update node. */
63-
class NeedsSyntheticPreUpdateNode extends PostUpdateNode {
64-
NeedsSyntheticPreUpdateNode() { this = objectCreationNode() }
65-
66-
override Node getPreUpdateNode() { result.(SyntheticPreUpdateNode).getPostUpdateNode() = this }
67-
68-
/**
69-
* Gets the label for this kind of node. This will figure in the textual representation of the synthesized pre-update node.
70-
*
71-
* There is currently only one reason for needing a pre-update node, so we always use that as the label.
72-
*/
73-
string label() { result = "objCreate" }
74-
}
75-
76-
/**
77-
* Calls to constructors are treated as post-update nodes for the synthesized argument
78-
* that is mapped to the `self` parameter. That way, constructor calls represent the value of the
79-
* object after the constructor (currently only `__init__`) has run.
80-
*/
81-
CfgNode objectCreationNode() {
82-
// TODO(call-graph): implement this!
83-
none()
84-
// result.getNode().(CallNode) = any(ClassCall c).getNode()
85-
}
54+
override Location getLocation() { result = node.getLocation() }
8655
}
8756

88-
import SyntheticPreUpdateNode
89-
90-
/** DEPRECATED: Alias for `SyntheticPostUpdateNode` */
91-
deprecated module syntheticPostUpdateNode = SyntheticPostUpdateNode;
92-
93-
/** A module collecting the different reasons for synthesising a post-update node. */
94-
module SyntheticPostUpdateNode {
95-
/** A post-update node is synthesized for all nodes which satisfy `NeedsSyntheticPostUpdateNode`. */
96-
class SyntheticPostUpdateNode extends PostUpdateNode, TSyntheticPostUpdateNode {
97-
NeedsSyntheticPostUpdateNode pre;
98-
99-
SyntheticPostUpdateNode() { this = TSyntheticPostUpdateNode(pre) }
57+
abstract class PostUpdateNodeImpl extends Node {
58+
/** Gets the node before the state update. */
59+
abstract Node getPreUpdateNode();
60+
}
10061

101-
override Node getPreUpdateNode() { result = pre }
62+
class SyntheticPostUpdateNode extends PostUpdateNodeImpl, TSyntheticPostUpdateNode {
63+
ControlFlowNode node;
10264

103-
override string toString() { result = "[post " + pre.label() + "] " + pre.toString() }
65+
SyntheticPostUpdateNode() { this = TSyntheticPostUpdateNode(node) }
10466

105-
override Scope getScope() { result = pre.getScope() }
67+
override Node getPreUpdateNode() { result.(CfgNode).getNode() = node }
10668

107-
override Location getLocation() { result = pre.getLocation() }
108-
}
69+
override string toString() { result = "[post] " + node.toString() }
10970

110-
/** A data flow node for which we should synthesise an associated post-update node. */
111-
class NeedsSyntheticPostUpdateNode extends Node {
112-
NeedsSyntheticPostUpdateNode() {
113-
this = argumentPreUpdateNode()
114-
or
115-
this = storePreUpdateNode()
116-
or
117-
this = readPreUpdateNode()
118-
}
71+
override Scope getScope() { result = node.getScope() }
11972

120-
/**
121-
* Gets the label for this kind of node. This will figure in the textual representation of the synthesized post-update node.
122-
* We favour being an arguments as the reason for the post-update node in case multiple reasons apply.
123-
*/
124-
string label() {
125-
if this = argumentPreUpdateNode()
126-
then result = "arg"
127-
else
128-
if this = storePreUpdateNode()
129-
then result = "store"
130-
else result = "read"
131-
}
132-
}
73+
override Location getLocation() { result = node.getLocation() }
74+
}
13375

134-
/**
135-
* Gets the pre-update node for this node.
136-
*
137-
* An argument might have its value changed as a result of a call.
138-
* Certain arguments, such as implicit self arguments are already post-update nodes
139-
* and should not have an extra node synthesised.
140-
*/
141-
Node argumentPreUpdateNode() {
142-
// TODO(call-graph): implement this!
143-
none()
144-
// result = any(FunctionCall c).getArg(_)
145-
// or
146-
// // Avoid argument 0 of method calls as those have read post-update nodes.
147-
// exists(MethodCall c, int n | n > 0 | result = c.getArg(n))
148-
// or
149-
// result = any(SpecialCall c).getArg(_)
150-
// or
151-
// // Avoid argument 0 of class calls as those have non-synthetic post-update nodes.
152-
// exists(ClassCall c, int n | n > 0 | result = c.getArg(n))
153-
// or
154-
// // any argument of any call that we have not been able to resolve
155-
// exists(CallNode call | not call = any(DataFlowCall c).getNode() |
156-
// result.(CfgNode).getNode() in [call.getArg(_), call.getArgByName(_)]
157-
// )
158-
}
76+
class NonSyntheticPostUpdateNode extends PostUpdateNodeImpl, CfgNode {
77+
SyntheticPreUpdateNode pre;
15978

160-
/** Gets the pre-update node associated with a store. This is used for when an object might have its value changed after a store. */
161-
CfgNode storePreUpdateNode() {
162-
exists(Attribute a |
163-
result.getNode() = a.getObject().getAFlowNode() and
164-
a.getCtx() instanceof Store
165-
)
166-
}
79+
NonSyntheticPostUpdateNode() { this = pre.getPostUpdateNode() }
16780

168-
/**
169-
* Gets a node marking the state change of an object after a read.
170-
*
171-
* A reverse read happens when the result of a read is modified, e.g. in
172-
* ```python
173-
* l = [ mutable ]
174-
* l[0].mutate()
175-
* ```
176-
* we may now have changed the content of `l`. To track this, there must be
177-
* a postupdate node for `l`.
178-
*/
179-
CfgNode readPreUpdateNode() {
180-
exists(Attribute a |
181-
result.getNode() = a.getObject().getAFlowNode() and
182-
a.getCtx() instanceof Load
183-
)
184-
or
185-
result.getNode() = any(SubscriptNode s).getObject()
186-
or
187-
// The dictionary argument is read from if the callable has parameters matching the keys.
188-
result.getNode().getNode() = any(Call call).getKwargs()
189-
}
81+
override Node getPreUpdateNode() { result = pre }
19082
}
19183

192-
import SyntheticPostUpdateNode
193-
19484
class DataFlowExpr = Expr;
19585

19686
/**

python/ql/lib/semmle/python/dataflow/new/internal/DataFlowPublic.qll

Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,41 @@ newtype TNode =
3131
or
3232
node.getNode() instanceof Pattern
3333
} or
34-
/** A synthetic node representing the value of an object before a state change */
35-
TSyntheticPreUpdateNode(NeedsSyntheticPreUpdateNode post) or
36-
/** A synthetic node representing the value of an object after a state change. */
37-
TSyntheticPostUpdateNode(NeedsSyntheticPostUpdateNode pre) or
34+
/**
35+
* A synthetic node representing the value of an object before a state change.
36+
*
37+
* For class calls we pass a synthetic self argument, so attribute writes in
38+
* `__init__` is reflected on the resulting object (we need special logic for this
39+
* since there is no `return` in `__init__`)
40+
*/
41+
// NOTE: since we can't rely on the call graph, but we want to have synthetic
42+
// pre-update nodes for class calls, we end up getting synthetic pre-update nodes for
43+
// ALL calls :|
44+
TSyntheticPreUpdateNode(CallNode call) or
45+
/**
46+
* A synthetic node representing the value of an object after a state change.
47+
* See QLDoc for `PostUpdateNode`.
48+
*/
49+
TSyntheticPostUpdateNode(ControlFlowNode node) {
50+
exists(CallNode call |
51+
node = call.getArg(_)
52+
or
53+
node = call.getArgByName(_)
54+
)
55+
or
56+
node = any(AttrNode a).getObject()
57+
or
58+
node = any(SubscriptNode s).getObject()
59+
or
60+
// self parameter when used implicitly in `super()`
61+
exists(Class cls, Function func, ParameterDefinition def |
62+
func = cls.getAMethod() and
63+
not hasStaticmethodDecorator(func) and
64+
// this matches what we do in ParameterNode
65+
def.getDefiningNode() = node and
66+
def.getParameter() = func.getArg(0)
67+
)
68+
} or
3869
/** A node representing a global (module-level) variable in a specific module. */
3970
TModuleVariableNode(Module m, GlobalVariable v) {
4071
v.getScope() = m and
@@ -270,13 +301,9 @@ class ExtractedParameterNode extends ParameterNodeImpl, CfgNode {
270301
ExtractedParameterNode() { node = def.getDefiningNode() }
271302

272303
override predicate isParameterOf(DataFlowCallable c, ParameterPosition ppos) {
273-
// TODO(call-graph): implement this!
274-
none()
304+
this = c.getParameter(ppos)
275305
}
276306

277-
override DataFlowCallable getEnclosingCallable() { this.isParameterOf(result, _) }
278-
279-
/** Gets the `Parameter` this `ParameterNode` represents. */
280307
override Parameter getParameter() { result = def.getParameter() }
281308
}
282309

@@ -294,16 +321,16 @@ abstract class ArgumentNode extends Node {
294321
final ExtractedDataFlowCall getCall() { this.argumentOf(result, _) }
295322
}
296323

297-
/** A data flow node that represents a call argument found in the source code. */
324+
/**
325+
* A data flow node that represents a call argument found in the source code,
326+
* where the call can be resolved.
327+
*/
298328
class ExtractedArgumentNode extends ArgumentNode {
299-
ExtractedArgumentNode() { this = any(ExtractedDataFlowCall c).getArgument(_) }
329+
ExtractedArgumentNode() { getCallArg(_, _, _, this, _) }
300330

301331
final override predicate argumentOf(DataFlowCall call, ArgumentPosition pos) {
302-
this.extractedArgumentOf(call, pos)
303-
}
304-
305-
predicate extractedArgumentOf(ExtractedDataFlowCall call, ArgumentPosition pos) {
306-
this = call.getArgument(pos)
332+
this = call.getArgument(pos) and
333+
call instanceof ExtractedDataFlowCall
307334
}
308335
}
309336

@@ -312,16 +339,17 @@ class ExtractedArgumentNode extends ArgumentNode {
312339
* changed its state.
313340
*
314341
* This can be either the argument to a callable after the callable returns
315-
* (which might have mutated the argument), or the qualifier of a field after
316-
* an update to the field.
342+
* (which might have mutated the argument), the qualifier of a field after
343+
* an update to the field, or a container such as a list/dictionary after an element
344+
* update.
317345
*
318346
* Nodes corresponding to AST elements, for example `ExprNode`s, usually refer
319-
* to the value before the update with the exception of `ObjectCreationNode`s,
347+
* to the value before the update with the exception of class calls,
320348
* which represents the value _after_ the constructor has run.
321349
*/
322-
abstract class PostUpdateNode extends Node {
350+
class PostUpdateNode extends Node instanceof PostUpdateNodeImpl {
323351
/** Gets the node before the state update. */
324-
abstract Node getPreUpdateNode();
352+
Node getPreUpdateNode() { result = super.getPreUpdateNode() }
325353
}
326354

327355
/**

python/ql/lib/semmle/python/dataflow/new/internal/TypeTrackerSpecific.qll

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -60,37 +60,24 @@ string getPossibleContentName() {
6060
result = any(DataFlowPublic::AttrRef a).getAttributeName()
6161
}
6262

63-
// /**
64-
// * Gets a callable for the call where `nodeFrom` is used as the `i`'th argument.
65-
// *
66-
// * Helper predicate to avoid bad join order experienced in `callStep`.
67-
// * This happened when `isParameterOf` was joined _before_ `getCallable`.
68-
// */
69-
// pragma[nomagic]
70-
// private DataFlowPrivate::DataFlowCallable getCallableForArgument(
71-
// DataFlowPublic::ExtractedArgumentNode nodeFrom, int i
72-
// ) {
73-
// exists(DataFlowPrivate::ExtractedDataFlowCall call |
74-
// nodeFrom.extractedArgumentOf(call, i) and
75-
// result = call.getCallable()
76-
// )
77-
// }
78-
7963
/**
8064
* Holds if `nodeFrom` steps to `nodeTo` by being passed as a parameter in a call.
8165
*
8266
* Flow into summarized library methods is not included, as that will lead to negative
8367
* recursion (or, at best, terrible performance), since identifying calls to library
8468
* methods is done using API graphs (which uses type tracking).
8569
*/
86-
predicate callStep(DataFlowPublic::ArgumentNode nodeFrom, DataFlowPrivate::ParameterNodeImpl nodeTo) {
87-
// TODO(call-graph): implement this!
88-
none()
89-
// // TODO: Support special methods?
90-
// exists(DataFlowPrivate::DataFlowCallable callable, int i |
91-
// callable = getCallableForArgument(nodeFrom, i) and
92-
// nodeTo.isParameterOf(callable, i)
93-
// )
70+
predicate callStep(DataFlowPublic::ArgumentNode nodeFrom, DataFlowPublic::ParameterNode nodeTo) {
71+
// TODO: Fix performance problem with pandas
72+
exists(
73+
DataFlowPrivate::DataFlowCall call, DataFlowPrivate::DataFlowCallable callable,
74+
DataFlowPrivate::ArgumentPosition apos, DataFlowPrivate::ParameterPosition ppos
75+
|
76+
nodeFrom = call.getArgument(apos) and
77+
nodeTo = callable.getParameter(ppos) and
78+
DataFlowPrivate::parameterMatch(ppos, apos) and
79+
callable = call.getCallable()
80+
)
9481
}
9582

9683
/** Holds if `nodeFrom` steps to `nodeTo` by being returned from a call. */

python/ql/src/meta/analysis-quality/CallGraphQuality.qll

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,15 +102,26 @@ module TypeTrackingBasedCallGraph {
102102

103103
/** A call that can be resolved by type-tracking. */
104104
class ResolvableCall extends RelevantCall {
105-
TT::DataFlowCallable dataflowTarget;
106-
107-
ResolvableCall() { dataflowTarget = TT::viableCallable(TT::TNormalCall(this)) }
105+
ResolvableCall() {
106+
exists(TT::TNormalCall(this, _, _))
107+
or
108+
TT::resolveClassCall(this, _)
109+
}
108110

109111
/** Gets a resolved target of this call. */
110112
Target getTarget() {
111-
result.(TargetFunction).getFunction() = dataflowTarget.(TT::DataFlowFunction).getScope()
112-
// TODO: class calls
113-
// result.(TargetClass).getClass()
113+
exists(TT::DataFlowCall call, TT::CallType ct, Function targetFunc |
114+
call = TT::TNormalCall(this, targetFunc, ct) and
115+
not ct instanceof TT::CallTypeClass and
116+
targetFunc = result.(TargetFunction).getFunction()
117+
)
118+
or
119+
// a TT::TNormalCall only exists when the call can be resolved to a function.
120+
// Since points-to just says the call goes directly to the class itself, and
121+
// type-tracking based wants to resolve this to the constructor, which might not
122+
// exist. So to do a proper comparison, we don't require the call to be resolve to
123+
// a specific function.
124+
TT::resolveClassCall(this, result.(TargetClass).getClass())
114125
}
115126
}
116127

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* @name New call graph edge from using type-tracking instead of points-to, that is ambiguous
3+
* @kind problem
4+
* @problem.severity recommendation
5+
* @id py/meta/call-graph-new-ambiguous
6+
* @tags meta
7+
* @precision very-low
8+
*/
9+
10+
import python
11+
import CallGraphQuality
12+
13+
from CallNode call, Target target
14+
where
15+
target.isRelevant() and
16+
not call.(PointsToBasedCallGraph::ResolvableCall).getTarget() = target and
17+
call.(TypeTrackingBasedCallGraph::ResolvableCall).getTarget() = target and
18+
1 < count(call.(TypeTrackingBasedCallGraph::ResolvableCall).getTarget())
19+
select call, "NEW: $@ to $@", call, "Call", target, target.toString()

0 commit comments

Comments
 (0)