Skip to content

Commit e301dd4

Browse files
Issues 38,39,40,41 solved
1 parent 4196d7e commit e301dd4

4 files changed

Lines changed: 296 additions & 53 deletions

File tree

src/codegraphcontext/server.py

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,11 @@ def _init_tools(self):
103103
},
104104
"analyze_code_relationships": {
105105
"name": "analyze_code_relationships",
106-
"description": "Analyze code relationships like 'who calls this function' or 'class hierarchy'. Supported query types include: find_callers, find_callees, find_importers, who_modifies, class_hierarchy, overrides, dead_code, call_chain, module_deps, variable_scope, find_complexity.",
106+
"description": "Analyze code relationships like 'who calls this function' or 'class hierarchy'. Supported query types include: find_callers, find_callees, find_all_callers, find_all_callees, find_importers, who_modifies, class_hierarchy, overrides, dead_code, call_chain, module_deps, variable_scope, find_complexity, find_functions_by_argument, find_functions_by_decorator.",
107107
"inputSchema": {
108108
"type": "object",
109109
"properties": {
110-
"query_type": {"type": "string", "description": "Type of relationship query to run."},
110+
"query_type": {"type": "string", "description": "Type of relationship query to run.", "enum": ["find_callers", "find_callees", "find_all_callers", "find_all_callees", "find_importers", "who_modifies", "class_hierarchy", "overrides", "dead_code", "call_chain", "module_deps", "variable_scope", "find_complexity", "find_functions_by_argument", "find_functions_by_decorator"]},
111111
"target": {"type": "string", "description": "The function, class, or module to analyze."},
112112
"context": {"type": "string", "description": "Optional: specific file path for precise results."}
113113
},
@@ -159,11 +159,12 @@ def _init_tools(self):
159159
},
160160
"find_dead_code": {
161161
"name": "find_dead_code",
162-
"description": "Find potentially unused functions (dead code) across the entire indexed codebase.",
162+
"description": "Find potentially unused functions (dead code) across the entire indexed codebase, optionally excluding functions with specific decorators.",
163163
"inputSchema": {
164164
"type": "object",
165-
"properties": {},
166-
"additionalProperties": False
165+
"properties": {
166+
"exclude_decorated_with": {"type": "array", "items": {"type": "string"}, "description": "Optional: A list of decorator names (e.g., '@app.route') to exclude from dead code detection.", "default": []}
167+
}
167168
}
168169
},
169170
"calculate_cyclomatic_complexity": {
@@ -303,12 +304,12 @@ def execute_cypher_query_tool(self, **args) -> Dict[str, Any]:
303304
"details": str(e)
304305
}
305306

306-
def find_dead_code_tool(self) -> Dict[str, Any]:
307+
def find_dead_code_tool(self, **args) -> Dict[str, Any]:
307308
"""Tool to find potentially dead code across the entire project."""
309+
exclude_decorated_with = args.get("exclude_decorated_with", [])
308310
try:
309311
debug_log("Finding dead code.")
310-
# The target argument from the old tool is not needed.
311-
results = self.code_finder.find_dead_code()
312+
results = self.code_finder.find_dead_code(exclude_decorated_with=exclude_decorated_with)
312313

313314
return {
314315
"success": True,
@@ -445,17 +446,11 @@ def list_imports_tool(self, **args) -> Dict[str, Any]:
445446
else:
446447
return {"error": f"Path {path} does not exist"}
447448

448-
if language == 'python':
449-
# Get the list of stdlib modules for the current Python version
450-
stdlib_modules = set(stdlibs.module_names)
451-
# stdlib_modules = {
452-
# 'os', 'sys', 'json', 'time', 'datetime', 'math', 'random', 're', 'collections',
453-
# 'itertools', 'functools', 'operator', 'pathlib', 'urllib', 'http', 'logging',
454-
# 'threading', 'multiprocessing', 'asyncio', 'typing', 'dataclasses', 'enum',
455-
# 'abc', 'io', 'csv', 'sqlite3', 'pickle', 'base64', 'hashlib', 'hmac', 'secrets',
456-
# 'unittest', 'doctest', 'pdb', 'profile', 'cProfile', 'timeit'
457-
# }
458-
all_imports = all_imports - stdlib_modules
449+
# Removed standard library filtering as per user request.
450+
# if language == 'python':
451+
# # Get the list of stdlib modules for the current Python version
452+
# stdlib_modules = set(stdlibs.module_names)
453+
# all_imports = all_imports - stdlib_modules
459454

460455
return {
461456
"imports": sorted(list(all_imports)), "language": language,

src/codegraphcontext/tools/code_finder.py

Lines changed: 145 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import logging
33
import re
44
from typing import Any, Dict, List
5+
from pathlib import Path
56

67
from ..core.database import DatabaseManager
78

@@ -116,6 +117,56 @@ def find_related_code(self, user_query: str) -> Dict[str, Any]:
116117

117118
return results
118119

120+
def find_functions_by_argument(self, argument_name: str, file_path: str = None) -> List[Dict]:
121+
"""Find functions that take a specific argument name."""
122+
with self.driver.session() as session:
123+
if file_path:
124+
query = """
125+
MATCH (f:Function)-[:HAS_PARAMETER]->(p:Parameter)
126+
WHERE p.name = $argument_name AND f.file_path = $file_path
127+
RETURN f.name AS function_name, f.file_path AS file_path, f.line_number AS line_number,
128+
f.docstring AS docstring, f.is_dependency AS is_dependency
129+
ORDER BY f.is_dependency ASC, f.file_path, f.line_number
130+
LIMIT 20
131+
"""
132+
result = session.run(query, argument_name=argument_name, file_path=file_path)
133+
else:
134+
query = """
135+
MATCH (f:Function)-[:HAS_PARAMETER]->(p:Parameter)
136+
WHERE p.name = $argument_name
137+
RETURN f.name AS function_name, f.file_path AS file_path, f.line_number AS line_number,
138+
f.docstring AS docstring, f.is_dependency AS is_dependency
139+
ORDER BY f.is_dependency ASC, f.file_path, f.line_number
140+
LIMIT 20
141+
"""
142+
result = session.run(query, argument_name=argument_name)
143+
return [dict(record) for record in result]
144+
145+
def find_functions_by_decorator(self, decorator_name: str, file_path: str = None) -> List[Dict]:
146+
"""Find functions that have a specific decorator applied to them."""
147+
with self.driver.session() as session:
148+
if file_path:
149+
query = """
150+
MATCH (f:Function)
151+
WHERE f.file_path = $file_path AND $decorator_name IN f.decorators
152+
RETURN f.name AS function_name, f.file_path AS file_path, f.line_number AS line_number,
153+
f.docstring AS docstring, f.is_dependency AS is_dependency, f.decorators AS decorators
154+
ORDER BY f.is_dependency ASC, f.file_path, f.line_number
155+
LIMIT 20
156+
"""
157+
result = session.run(query, decorator_name=decorator_name, file_path=file_path)
158+
else:
159+
query = """
160+
MATCH (f:Function)
161+
WHERE $decorator_name IN f.decorators
162+
RETURN f.name AS function_name, f.file_path AS file_path, f.line_number AS line_number,
163+
f.docstring AS docstring, f.is_dependency AS is_dependency, f.decorators AS decorators
164+
ORDER BY f.is_dependency ASC, f.file_path, f.line_number
165+
LIMIT 20
166+
"""
167+
result = session.run(query, decorator_name=decorator_name)
168+
return [dict(record) for record in result]
169+
119170
def who_calls_function(self, function_name: str, file_path: str = None) -> List[Dict]:
120171
"""Find what functions call a specific function using CALLS relationships with improved matching"""
121172
with self.driver.session() as session:
@@ -230,7 +281,7 @@ def who_imports_module(self, module_name: str) -> List[Dict]:
230281
with self.driver.session() as session:
231282
result = session.run("""
232283
MATCH (file:File)-[imp:IMPORTS]->(module:Module)
233-
WHERE module.name CONTAINS $module_name OR module.name = $module_name
284+
WHERE module.name = $module_name OR module.full_import_name CONTAINS $module_name
234285
OPTIONAL MATCH (repo:Repository)-[:CONTAINS]->(file)
235286
RETURN DISTINCT
236287
file.name as file_name,
@@ -344,15 +395,19 @@ def find_function_overrides(self, function_name: str) -> List[Dict]:
344395

345396
return [dict(record) for record in result]
346397

347-
def find_dead_code(self) -> Dict[str, Any]:
348-
"""Find potentially unused functions (not called by other functions in the project)"""
398+
def find_dead_code(self, exclude_decorated_with: List[str] = None) -> Dict[str, Any]:
399+
"""Find potentially unused functions (not called by other functions in the project), optionally excluding those with specific decorators."""
400+
if exclude_decorated_with is None:
401+
exclude_decorated_with = []
402+
349403
with self.driver.session() as session:
350404
result = session.run("""
351405
MATCH (func:Function)
352406
WHERE func.is_dependency = false
353407
AND NOT func.name IN ['main', '__init__', '__main__', 'setup', 'run', '__new__', '__del__']
354408
AND NOT func.name STARTS WITH '_test'
355409
AND NOT func.name STARTS WITH 'test_'
410+
AND ALL(decorator_name IN $exclude_decorated_with WHERE NOT decorator_name IN func.decorators)
356411
WITH func
357412
OPTIONAL MATCH (caller:Function)-[:CALLS]->(func)
358413
WHERE caller.is_dependency = false
@@ -368,37 +423,84 @@ def find_dead_code(self) -> Dict[str, Any]:
368423
file.name as file_name
369424
ORDER BY func.file_path, func.line_number
370425
LIMIT 50
371-
""")
426+
""", exclude_decorated_with=exclude_decorated_with)
372427

373428
return {
374429
"potentially_unused_functions": [dict(record) for record in result],
375430
"note": "These functions might be unused, but could be entry points, callbacks, or called dynamically"
376431
}
377432

433+
def find_all_callers(self, function_name: str, file_path: str = None) -> List[Dict]:
434+
"""Find all direct and indirect callers of a specific function."""
435+
with self.driver.session() as session:
436+
if file_path:
437+
# Find functions within the specified file_path that call the target function
438+
query = """
439+
MATCH (f:Function)-[:CALLS*]->(target:Function {name: $function_name})
440+
WHERE f.file_path = $file_path
441+
RETURN DISTINCT f.name AS caller_name, f.file_path AS caller_file_path, f.line_number AS caller_line_number, f.is_dependency AS caller_is_dependency
442+
ORDER BY f.is_dependency ASC, f.file_path, f.line_number
443+
LIMIT 50
444+
"""
445+
result = session.run(query, function_name=function_name, file_path=file_path)
446+
else:
447+
# If no file_path (context) is provided, find all callers of the function by name
448+
query = """
449+
MATCH (f:Function)-[:CALLS*]->(target:Function {name: $function_name})
450+
RETURN DISTINCT f.name AS caller_name, f.file_path AS caller_file_path, f.line_number AS caller_line_number, f.is_dependency AS caller_is_dependency
451+
ORDER BY f.is_dependency ASC, f.file_path, f.line_number
452+
LIMIT 50
453+
"""
454+
result = session.run(query, function_name=function_name)
455+
return [dict(record) for record in result]
456+
457+
def find_all_callees(self, function_name: str, file_path: str = None) -> List[Dict]:
458+
"""Find all direct and indirect callees of a specific function."""
459+
with self.driver.session() as session:
460+
if file_path:
461+
query = """
462+
MATCH (caller:Function {name: $function_name, file_path: $file_path})
463+
MATCH (caller)-[:CALLS*]->(f:Function)
464+
RETURN DISTINCT f.name AS callee_name, f.file_path AS callee_file_path, f.line_number AS callee_line_number, f.is_dependency AS callee_is_dependency
465+
ORDER BY f.is_dependency ASC, f.file_path, f.line_number
466+
LIMIT 50
467+
"""
468+
result = session.run(query, function_name=function_name, file_path=file_path)
469+
else:
470+
query = """
471+
MATCH (caller:Function {name: $function_name})
472+
MATCH (caller)-[:CALLS*]->(f:Function)
473+
RETURN DISTINCT f.name AS callee_name, f.file_path AS callee_file_path, f.line_number AS callee_line_number, f.is_dependency AS callee_is_dependency
474+
ORDER BY f.is_dependency ASC, f.file_path, f.line_number
475+
LIMIT 50
476+
"""
477+
result = session.run(query, function_name=function_name)
478+
return [dict(record) for record in result]
479+
378480
def find_function_call_chain(self, start_function: str, end_function: str, max_depth: int = 5) -> List[Dict]:
379481
"""Find call chains between two functions"""
380482
with self.driver.session() as session:
381-
result = session.run("""
483+
result = session.run(f"""
382484
MATCH path = shortestPath(
383-
(start:Function {name: $start_function})-[:CALLS*1..$max_depth]->(end:Function {name: $end_function})
485+
(start:Function {{name: $start_function}})-[:CALLS*1..{max_depth}]->(end:Function {{name: $end_function}})
384486
)
385487
WITH path, nodes(path) as func_nodes, relationships(path) as call_rels
386488
RETURN
387-
[node in func_nodes | {
489+
[node in func_nodes | {{
388490
name: node.name,
389491
file_path: node.file_path,
390492
line_number: node.line_number,
391493
is_dependency: node.is_dependency
392-
}] as function_chain,
393-
[rel in call_rels | {
494+
}}] as function_chain,
495+
[rel in call_rels | {{
394496
call_line: rel.line_number,
395497
args: rel.args,
396498
full_call_name: rel.full_call_name
397-
}] as call_details,
499+
}}] as call_details,
398500
length(path) as chain_length
399501
ORDER BY chain_length ASC
400502
LIMIT 10
401-
""", start_function=start_function, end_function=end_function, max_depth=max_depth)
503+
""", start_function=start_function, end_function=end_function)
402504

403505
return [dict(record) for record in result]
404506

@@ -494,6 +596,20 @@ def analyze_code_relationships(self, query_type: str, target: str, context: str
494596
"summary": f"Found {len(results)} files that import '{target}'"
495597
}
496598

599+
elif query_type == "find_functions_by_argument":
600+
results = self.find_functions_by_argument(target, context)
601+
return {
602+
"query_type": "find_functions_by_argument", "target": target, "context": context, "results": results,
603+
"summary": f"Found {len(results)} functions that take '{target}' as an argument"
604+
}
605+
606+
elif query_type == "find_functions_by_decorator":
607+
results = self.find_functions_by_decorator(target, context)
608+
return {
609+
"query_type": "find_functions_by_decorator", "target": target, "context": context, "results": results,
610+
"summary": f"Found {len(results)} functions decorated with '{target}'"
611+
}
612+
497613
elif query_type in ["who_modifies", "modifies", "mutations", "changes", "variable_usage"]:
498614
results = self.who_modifies_variable(target)
499615
return {
@@ -530,13 +646,29 @@ def analyze_code_relationships(self, query_type: str, target: str, context: str
530646
"summary": f"Found the top {len(results)} most complex functions"
531647
}
532648

649+
elif query_type == "find_all_callers":
650+
results = self.find_all_callers(target, context)
651+
return {
652+
"query_type": "find_all_callers", "target": target, "context": context, "results": results,
653+
"summary": f"Found {len(results)} direct and indirect callers of '{target}'"
654+
}
655+
656+
elif query_type == "find_all_callees":
657+
results = self.find_all_callees(target, context)
658+
return {
659+
"query_type": "find_all_callees", "target": target, "context": context, "results": results,
660+
"summary": f"Found {len(results)} direct and indirect callees of '{target}'"
661+
}
662+
533663
elif query_type in ["call_chain", "path", "chain"]:
534664
if '->' in target:
535665
start_func, end_func = target.split('->', 1)
536-
results = self.find_function_call_chain(start_func.strip(), end_func.strip())
666+
# max_depth can be passed as context, default to 5 if not provided or invalid
667+
max_depth = int(context) if context and context.isdigit() else 5
668+
results = self.find_function_call_chain(start_func.strip(), end_func.strip(), max_depth)
537669
return {
538670
"query_type": "call_chain", "target": target, "results": results,
539-
"summary": f"Found {len(results)} call chains from '{start_func.strip()}' to '{end_func.strip()}'"
671+
"summary": f"Found {len(results)} call chains from '{start_func.strip()}' to '{end_func.strip()}' (max depth: {max_depth})"
540672
}
541673
else:
542674
return {

0 commit comments

Comments
 (0)