(Python) Resolve FQNs for references
Assuming we have a set of references captured by ast-grep, the next step is to compute a fully qualified name (FQN) for each one. This is significantly more complex than computing FQNs for definitions and, due to Python's dynamic nature, perfect static resolution will not be possible.
We can achieve "pretty good" resolution, though, by combining a variety of analysis techniques, like scope tracking, data flow analysis, type inference, inheritance resolution, etc. There are many cases to handle, ranging from common to rare/complex. Below are some examples of the trickier cases. As work on this progresses, we can decide which of these to handle in the first iteration based on the effort.
Note: If a reference is of an imported function, fully resolving the location of the function definition is out of scope (no pun intended) for this issue. That work will happen in the Knowledge Graph Indexer, and will involve building an import graph from all parsed files in a codebase.
Examples
Variable reassignments:
def foo():
pass
def bar():
pass
# Simple reassignment
fizz = foo
fizz()
# Multiple reassignments
fizz = foo
buzz = fizz
buzz()
# Conditional reassignment
fizz = foo if condition else bar
fizz() # foo or bar? Ambiguous
Higher-order functions:
# Functions returning functions
def get_fn():
if condition:
return foo
else:
return bar
my_fn = get_fn()
my_fn() # foo or bar? Ambiguous
# Callbacks
def call_fn(func):
return func() # foo or bar? Ambiguous
call_fn(foo)
call_fn(bar)
# Closures
def outer():
def inner():
pass
return inner
fn = outer()
fn() # outer.<locals>.inner
Dynamic attribute access:
funcs = {
"foo": foo,
"bar": bar
}
func = funcs["foo"]
func()
Aliased imports:
from utils.stuff import foo as bar # Alias + shadowing
bar() # utils.stuff.foo
Method resolution:
class MyClass:
@property
def my_property(self):
return self._my_property
@staticmethod
def static_method():
pass
@classmethod
def class_method(cls):
pass
def instance_method(self):
pass
# Different ways to call
MyClass.static_method() # MyClass.static_method
MyClass.class_method() # MyClass.class_method
my_class = MyClass() # MyClass.__call__
my_class.instance_method() # MyClass.instance_method
my_class.static_method() # MyClass.static_method
my_class.my_property # MyClass.my_property.fget
Inheritance and super calls:
class Base:
def foo(self):
pass
class Child(Base):
def foo(self):
super().foo() # Base.foo
def bar(self):
self.foo() # Child.foo (not Base.foo)
This list of examples is not comprehensive and there are many more cases that will pop up. Whenever a decision is made to not handle a case, we'll note it in the limitations section of the parent epic.