Skip to content

Commit dd38541

Browse files
authored
(Code quality) - Ban recursive functions in codebase (#7910)
* code qa add RecursiveFunctionFinder * test_recursive_detector * RecursiveFunctionFinder * fix check * recursive_detector
1 parent b6f2e65 commit dd38541

File tree

2 files changed

+90
-0
lines changed

2 files changed

+90
-0
lines changed

.circleci/config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -992,6 +992,7 @@ jobs:
992992
- run: ruff check ./litellm
993993
# - run: python ./tests/documentation_tests/test_general_setting_keys.py
994994
- run: python ./tests/code_coverage_tests/router_code_coverage.py
995+
- run: python ./tests/code_coverage_tests/recursive_detector.py
995996
- run: python ./tests/code_coverage_tests/test_router_strategy_async.py
996997
- run: python ./tests/code_coverage_tests/litellm_logging_code_coverage.py
997998
- run: python ./tests/documentation_tests/test_env_keys.py
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import ast
2+
import os
3+
4+
IGNORE_FUNCTIONS = [
5+
"_format_type",
6+
"_remove_additional_properties",
7+
"_remove_strict_from_schema",
8+
"text_completion",
9+
"_check_for_os_environ_vars",
10+
"clean_message",
11+
"_prepare_metadata",
12+
"unpack_defs",
13+
"convert_to_nullable",
14+
"add_object_type",
15+
"strip_field",
16+
"_transform_prompt",
17+
]
18+
19+
20+
class RecursiveFunctionFinder(ast.NodeVisitor):
21+
def __init__(self):
22+
self.recursive_functions = []
23+
self.ignored_recursive_functions = []
24+
25+
def visit_FunctionDef(self, node):
26+
# Check if the function calls itself
27+
if any(self._is_recursive_call(node, call) for call in ast.walk(node)):
28+
if node.name in IGNORE_FUNCTIONS:
29+
self.ignored_recursive_functions.append(node.name)
30+
else:
31+
self.recursive_functions.append(node.name)
32+
self.generic_visit(node)
33+
34+
def _is_recursive_call(self, func_node, call_node):
35+
# Check if the call node is a function call
36+
if not isinstance(call_node, ast.Call):
37+
return False
38+
39+
# Case 1: Direct function call (e.g., my_func())
40+
if isinstance(call_node.func, ast.Name) and call_node.func.id == func_node.name:
41+
return True
42+
43+
# Case 2: Method call with self (e.g., self.my_func())
44+
if isinstance(call_node.func, ast.Attribute) and isinstance(
45+
call_node.func.value, ast.Name
46+
):
47+
return (
48+
call_node.func.value.id == "self"
49+
and call_node.func.attr == func_node.name
50+
)
51+
52+
return False
53+
54+
55+
def find_recursive_functions_in_file(file_path):
56+
with open(file_path, "r") as file:
57+
tree = ast.parse(file.read(), filename=file_path)
58+
finder = RecursiveFunctionFinder()
59+
finder.visit(tree)
60+
return finder.recursive_functions, finder.ignored_recursive_functions
61+
62+
63+
def find_recursive_functions_in_directory(directory):
64+
recursive_functions = {}
65+
ignored_recursive_functions = {}
66+
for root, _, files in os.walk(directory):
67+
for file in files:
68+
print("file: ", file)
69+
if file.endswith(".py"):
70+
file_path = os.path.join(root, file)
71+
functions, ignored = find_recursive_functions_in_file(file_path)
72+
if functions:
73+
recursive_functions[file_path] = functions
74+
if ignored:
75+
ignored_recursive_functions[file_path] = ignored
76+
return recursive_functions, ignored_recursive_functions
77+
78+
79+
# Example usage
80+
directory_path = "./litellm"
81+
recursive_functions, ignored_recursive_functions = (
82+
find_recursive_functions_in_directory(directory_path)
83+
)
84+
print("ALL RECURSIVE FUNCTIONS: ", recursive_functions)
85+
print("IGNORED RECURSIVE FUNCTIONS: ", ignored_recursive_functions)
86+
if len(recursive_functions) > 0:
87+
raise Exception(
88+
f"🚨 Recursive functions found in {file}: {functions}. THIS IS REALLY BAD, it has caused CPU Usage spikes in the past. Only keep this if it's ABSOLUTELY necessary."
89+
)

0 commit comments

Comments
 (0)