Skip to content

Commit d77f368

Browse files
committed
lint
1 parent 7aa4d45 commit d77f368

File tree

3 files changed

+169
-17
lines changed

3 files changed

+169
-17
lines changed

src/codegen/sdk/core/detached_symbols/function_call.py

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,28 @@ def __init__(self, node: TSNode, file_node_id: NodeId, G: CodebaseGraph, parent:
6262
args = [Argument(x, i, self) for i, x in enumerate(arg_list_node.named_children) if x.type != "comment"]
6363
self._arg_list = Collection(arg_list_node, self.file_node_id, self.G, self, children=args)
6464

65+
def __repr__(self) -> str:
66+
"""Custom string representation showing the function call chain structure.
67+
68+
Format: FunctionCall(name=current, pred=pred_name, succ=succ_name, base=base_name)
69+
70+
It will only print out predecessor, successor, and base that are of type FunctionCall. If it's a property, it will not be logged
71+
"""
72+
# Helper to safely get name
73+
74+
# Get names for each part
75+
parts = [f"name='{self.name}'"]
76+
77+
if self.predecessor and isinstance(self.predecessor, FunctionCall):
78+
parts.append(f"predecessor=FunctionCall(name='{self.predecessor.name}')")
79+
80+
if self.successor and isinstance(self.successor, FunctionCall):
81+
parts.append(f"successor=FunctionCall(name='{self.successor.name}')")
82+
83+
parts.append(f"filepath='{self.file.filepath}'")
84+
85+
return f"FunctionCall({', '.join(parts)})"
86+
6587
@classmethod
6688
def from_usage(cls, node: Editable[Parent], parent: Parent | None = None) -> Self | None:
6789
"""Creates a FunctionCall object from an Editable instance that represents a function call.
@@ -210,9 +232,36 @@ def predecessor(self) -> FunctionCall[Parent] | None:
210232
or if the predecessor is not a function call.
211233
"""
212234
# Recursively travel down the tree to find the previous function call (child nodes are previous calls)
213-
return self.call_chain[-2] if len(self.call_chain) > 1 else None
235+
name = self.get_name()
236+
while name:
237+
if isinstance(name, FunctionCall):
238+
return name
239+
elif isinstance(name, ChainedAttribute):
240+
name = name.object
241+
else:
242+
break
243+
return None
244+
245+
@property
246+
@reader
247+
def successor(self) -> FunctionCall[Parent] | Name | None:
248+
"""Returns the next function call in a function call chain.
249+
250+
Returns the next function call in a function call chain. This method is useful for traversing function call chains
251+
to analyze or modify sequences of chained function calls.
214252
215-
# TODO: also define a successor?
253+
Args:
254+
None
255+
256+
Returns:
257+
FunctionCall[Parent] | Name | None: The next function call in the chain, or None if there is no successor
258+
or if the successor is not a function call.
259+
"""
260+
# this will avoid parent function calls in tree-sitter that are NOT part of the chained calls
261+
if not isinstance(self.parent, ChainedAttribute):
262+
return None
263+
264+
return self.parent_of_type(FunctionCall)
216265

217266
@property
218267
@noapidoc
@@ -601,24 +650,35 @@ def register_api_call(self, url: str):
601650
@property
602651
@reader
603652
def call_chain(self) -> list[FunctionCall]:
604-
"""Returns a list of all function calls in this function call chain, including this call. Does not include calls made after this one."""
653+
"""Returns a list of all function calls in this function call chain, including this call. Does not include calls made after this one."""
605654
ret = []
606-
name = self.get_name()
607-
while name:
608-
if isinstance(name, FunctionCall):
609-
ret.extend(name.call_chain)
610-
break
611-
elif isinstance(name, ChainedAttribute):
612-
name = name.object
613-
else:
614-
break
655+
656+
# backward traversal
657+
curr = self
658+
pred = curr.predecessor
659+
while pred is not None and isinstance(pred, FunctionCall):
660+
ret.insert(0, pred)
661+
pred = pred.predecessor
662+
615663
ret.append(self)
664+
665+
# forward traversal
666+
curr = self
667+
succ = curr.successor
668+
while succ is not None and isinstance(succ, FunctionCall):
669+
ret.append(succ)
670+
succ = succ.successor
671+
616672
return ret
617673

618674
@property
619675
@reader
620676
def base(self) -> Editable | None:
621-
"""Returns the base object of this function call chain."""
677+
"""Returns the base object of this function call chain.
678+
679+
Args:
680+
Editable | None: The base object of this function call chain.
681+
"""
622682
name = self.get_name()
623683
while isinstance(name, ChainedAttribute):
624684
if isinstance(name.object, FunctionCall):

src/codegen/sdk/core/interfaces/editable.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ def _is_empty_container(text: str) -> bool:
7575
"resolved_types",
7676
"valid_symbol_names",
7777
"valid_import_names",
78+
"predecessor",
79+
"successor",
80+
"base",
81+
"call_chain",
7882
"code_block",
7983
"parent_statement",
8084
"symbol_usages",

tests/unit/codegen/sdk/python/detached_symbols/function_call/test_function_call.py

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -502,8 +502,8 @@ def baz():
502502

503503
# Check call chain
504504
assert c.call_chain == [a, b, c]
505-
assert b.call_chain == [a, b]
506-
assert a.call_chain == [a]
505+
assert b.call_chain == [a, b, c]
506+
assert a.call_chain == [a, b, c]
507507

508508
# Check base
509509
assert c.base == a.get_name()
@@ -530,15 +530,103 @@ def baz():
530530

531531
# Check call chain
532532
assert c.call_chain == [a, b, c]
533-
assert b.call_chain == [a, b]
534-
assert a.call_chain == [a]
533+
assert b.call_chain == [a, b, c]
534+
assert a.call_chain == [a, b, c]
535535

536536
# Check base
537537
assert c.base.source == "x"
538538
assert b.base.source == "x"
539539
assert a.base.source == "x"
540540

541541

542+
def test_function_call_chain_nested(tmpdir) -> None:
543+
# language=python
544+
content = """
545+
def foo():
546+
# Nested function calls - each call should be independent
547+
a(b(c()))
548+
"""
549+
with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}) as codebase:
550+
file = codebase.get_file("test.py")
551+
foo = file.get_function("foo")
552+
calls = foo.function_calls
553+
assert len(calls) == 3
554+
a = calls[0]
555+
b = calls[1]
556+
c = calls[2]
557+
558+
# Each call should be independent - no predecessors
559+
assert a.predecessor is None
560+
assert b.predecessor is None
561+
assert c.predecessor is None
562+
563+
# No successors since they're nested, not chained
564+
assert a.successor is None
565+
assert b.successor is None
566+
assert c.successor is None
567+
568+
# Call chain for each should only include itself
569+
assert a.call_chain == [a]
570+
assert b.call_chain == [b]
571+
assert c.call_chain == [c]
572+
573+
# Verify source strings are correct
574+
assert a.source == "a(b(c()))"
575+
assert b.source == "b(c())"
576+
assert c.source == "c()"
577+
578+
579+
def test_function_call_chain_successor(tmpdir) -> None:
580+
# language=python
581+
content = """
582+
def foo():
583+
a().b().c()
584+
585+
def bat():
586+
x.y.z.func()
587+
588+
def baz():
589+
x.a().y.b().z.c()
590+
"""
591+
with get_codebase_session(tmpdir=tmpdir, files={"test.py": content}) as codebase:
592+
file = codebase.get_file("test.py")
593+
594+
# Check foo
595+
foo = file.get_function("foo")
596+
calls = foo.function_calls
597+
assert len(calls) == 3
598+
c = calls[0]
599+
b = calls[1]
600+
a = calls[2]
601+
602+
# Check successors
603+
assert a.successor == b
604+
assert b.successor == c
605+
assert c.successor is None
606+
607+
# Check bat
608+
bat = file.get_function("bat")
609+
calls = bat.function_calls
610+
assert len(calls) == 1
611+
func = calls[0]
612+
613+
# No successor since it's a single function call
614+
assert func.successor is None
615+
616+
# Check baz
617+
baz = file.get_function("baz")
618+
calls = baz.function_calls
619+
assert len(calls) == 3
620+
c = calls[0]
621+
b = calls[1]
622+
a = calls[2]
623+
624+
# Check successors
625+
assert a.successor == b
626+
assert b.successor == c
627+
assert c.successor is None
628+
629+
542630
def test_function_call_chain_hard(tmpdir) -> None:
543631
# language=python
544632
content = """

0 commit comments

Comments
 (0)