Skip to content

Commit e4451aa

Browse files
Merge pull request CodeGraphContext#238 from Shashankss1205/js_enh
added visualizer tool
2 parents a73b997 + 7dff72f commit e4451aa

3 files changed

Lines changed: 112 additions & 65 deletions

File tree

src/codegraphcontext/server.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# src/codegraphcontext/server.py
2+
import urllib.parse
23
import asyncio
34
import json
45
import logging
@@ -223,6 +224,17 @@ def _init_tools(self):
223224
},
224225
"required": ["repo_path"]
225226
}
227+
},
228+
"visualize_graph_query": {
229+
"name": "visualize_graph_query",
230+
"description": "Generates a URL to visualize the results of a Cypher query in the Neo4j Browser. The user can open this URL in their web browser to see the graph visualization.",
231+
"inputSchema": {
232+
"type": "object",
233+
"properties": {
234+
"cypher_query": {"type": "string", "description": "The Cypher query to visualize."}
235+
},
236+
"required": ["cypher_query"]
237+
}
226238
}
227239
}
228240

@@ -424,6 +436,25 @@ def delete_repository_tool(self, **args) -> Dict[str, Any]:
424436
debug_log(f"Error deleting repository: {str(e)}")
425437
return {"error": f"Failed to delete repository: {str(e)}"}
426438

439+
def visualize_graph_query_tool(self, **args) -> Dict[str, Any]:
440+
"""Tool to generate a Neo4j browser visualization URL for a Cypher query."""
441+
cypher_query = args.get("cypher_query")
442+
if not cypher_query:
443+
return {"error": "Cypher query cannot be empty."}
444+
445+
try:
446+
encoded_query = urllib.parse.quote(cypher_query)
447+
visualization_url = f"http://localhost:7474/browser/?cmd=edit&arg={encoded_query}"
448+
449+
return {
450+
"success": True,
451+
"visualization_url": visualization_url,
452+
"message": "Open the URL in your browser to visualize the graph query. The query will be pre-filled for editing."
453+
}
454+
except Exception as e:
455+
debug_log(f"Error generating visualization URL: {str(e)}")
456+
return {"error": f"Failed to generate visualization URL: {str(e)}"}
457+
427458
def watch_directory_tool(self, **args) -> Dict[str, Any]:
428459
"""
429460
Tool implementation to start watching a directory for changes.
@@ -746,7 +777,8 @@ async def handle_tool_call(self, tool_name: str, args: Dict[str, Any]) -> Dict[s
746777
"calculate_cyclomatic_complexity": self.calculate_cyclomatic_complexity_tool,
747778
"find_most_complex_functions": self.find_most_complex_functions_tool,
748779
"list_indexed_repositories": self.list_indexed_repositories_tool,
749-
"delete_repository": self.delete_repository_tool
780+
"delete_repository": self.delete_repository_tool,
781+
"visualize_graph_query": self.visualize_graph_query_tool
750782
}
751783
handler = tool_map.get(tool_name)
752784
if handler:

src/codegraphcontext/tools/graph_builder.py

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -209,18 +209,38 @@ def add_file_to_graph(self, file_data: Dict, repo_name: str, imports_map: dict):
209209
""", context=item["context"], file_path=file_path_str, name=item["name"], line_number=item["line_number"])
210210

211211
# Handle imports and create IMPORTS relationships
212-
for imp in file_data['imports']:
213-
session.run("""
214-
MATCH (f:File {path: $file_path})
215-
MERGE (m:Module {name: $name})
216-
ON CREATE SET m.full_import_name = $full_import_name
217-
ON MATCH SET m.full_import_name = $full_import_name
218-
MERGE (f)-[:IMPORTS {alias: $rel_alias}]->(m)
219-
""",
220-
file_path=file_path_str,
221-
name=imp.get('name'),
222-
full_import_name=imp.get('full_import_name', imp.get('name')),
223-
rel_alias=imp.get('alias'))
212+
for imp in file_data.get('imports', []):
213+
logger.info(f"Processing import: {imp}")
214+
lang = file_data.get('lang')
215+
if lang == 'javascript':
216+
# New, correct logic for JS
217+
module_name = imp.get('source')
218+
if not module_name: continue
219+
220+
# Use a map for relationship properties to handle optional alias
221+
rel_props = {'imported_name': imp.get('name', '*')}
222+
if imp.get('alias'):
223+
rel_props['alias'] = imp.get('alias')
224+
225+
session.run("""
226+
MATCH (f:File {path: $file_path})
227+
MERGE (m:Module {name: $module_name})
228+
MERGE (f)-[r:IMPORTS]->(m)
229+
SET r += $props
230+
""", file_path=file_path_str, module_name=module_name, props=rel_props)
231+
else:
232+
# Existing logic for Python (and other languages)
233+
set_clauses = ["m.alias = $alias"]
234+
if 'full_import_name' in imp:
235+
set_clauses.append("m.full_import_name = $full_import_name")
236+
set_clause_str = ", ".join(set_clauses)
237+
238+
session.run(f"""
239+
MATCH (f:File {{path: $file_path}})
240+
MERGE (m:Module {{name: $name}})
241+
SET {set_clause_str}
242+
MERGE (f)-[:IMPORTS]->(m)
243+
""", file_path=file_path_str, **imp)
224244

225245
# Handle CONTAINS relationship between class to their children like variables
226246
for func in file_data.get('functions', []):

src/codegraphcontext/tools/languages/javascript.py

Lines changed: 47 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -344,61 +344,56 @@ def _find_imports(self, root_node):
344344
imports = []
345345
query = self.queries['imports']
346346
for node, capture_name in query.captures(root_node):
347-
if capture_name == 'import':
348-
line_number = node.start_point[0] + 1
349-
if node.type == 'import_statement':
350-
source_node = node.child_by_field_name('source')
351-
if not source_node: continue
352-
source = self._get_node_text(source_node).strip('\'"')
347+
if capture_name != 'import':
348+
continue
353349

354-
import_clause = next((c for c in node.children if c.type == 'import_clause'), None)
355-
if not import_clause: # e.g. import "source"
356-
imports.append({"name": source, "full_import_name": source, "alias": source, "line_number": line_number, "context": None, "lang": self.language_name, "is_dependency": False})
357-
continue
358-
359-
# default import
360-
default_node = next((c for c in import_clause.children if c.type == 'identifier'), None)
361-
if default_node:
362-
alias = self._get_node_text(default_node)
363-
imports.append({"name": "default", "full_import_name": source, "alias": alias, "line_number": line_number, "context": None, "lang": self.language_name, "is_dependency": False})
350+
line_number = node.start_point[0] + 1
351+
352+
if node.type == 'import_statement':
353+
source = self._get_node_text(node.child_by_field_name('source')).strip('\'"')
354+
355+
# Look for different import structures
356+
import_clause = node.child_by_field_name('import')
357+
if not import_clause:
358+
imports.append({'name': source, 'source': source, 'alias': None, 'line_number': line_number, 'lang': self.language_name})
359+
continue
360+
361+
# Default import: import defaultExport from '...'
362+
if import_clause.type == 'identifier':
363+
alias = self._get_node_text(import_clause)
364+
imports.append({'name': 'default', 'source': source, 'alias': alias, 'line_number': line_number, 'lang': self.language_name})
364365

365-
# namespace import
366-
namespace_node = next((c for c in import_clause.children if c.type == 'namespace_import'), None)
367-
if namespace_node:
368-
alias_node = None
369-
for child in namespace_node.children:
370-
if child.type == 'identifier':
371-
alias_node = child
372-
break
373-
if alias_node:
374-
alias = self._get_node_text(alias_node)
375-
imports.append({"name": "*", "full_import_name": source, "alias": alias, "line_number": line_number, "context": None, "lang": self.language_name, "is_dependency": False})
376-
377-
# named imports
378-
named_imports_node = next((c for c in import_clause.children if c.type == 'named_imports'), None)
379-
if named_imports_node:
380-
for specifier in named_imports_node.children:
381-
if specifier.type == 'import_specifier':
382-
name_node = specifier.child_by_field_name('name')
383-
alias_node = specifier.child_by_field_name('alias')
384-
name = self._get_node_text(name_node)
385-
alias = self._get_node_text(alias_node) if alias_node else name
386-
imports.append({"name": name, "full_import_name": f"{source}/{name}", "alias": alias, "line_number": line_number, "context": None, "lang": self.language_name, "is_dependency": False})
366+
# Namespace import: import * as name from '...'
367+
elif import_clause.type == 'namespace_import':
368+
alias_node = import_clause.child_by_field_name('alias')
369+
if alias_node:
370+
alias = self._get_node_text(alias_node)
371+
imports.append({'name': '*', 'source': source, 'alias': alias, 'line_number': line_number, 'lang': self.language_name})
372+
373+
# Named imports: import { name, name as alias } from '...'
374+
elif import_clause.type == 'named_imports':
375+
for specifier in import_clause.children:
376+
if specifier.type == 'import_specifier':
377+
name_node = specifier.child_by_field_name('name')
378+
alias_node = specifier.child_by_field_name('alias')
379+
original_name = self._get_node_text(name_node)
380+
alias = self._get_node_text(alias_node) if alias_node else None
381+
imports.append({'name': original_name, 'source': source, 'alias': alias, 'line_number': line_number, 'lang': self.language_name})
382+
383+
elif node.type == 'call_expression': # require('...')
384+
args = node.child_by_field_name('arguments')
385+
if not args or args.named_child_count == 0: continue
386+
source_node = args.named_child(0)
387+
if not source_node or source_node.type != 'string': continue
388+
source = self._get_node_text(source_node).strip('\'"')
389+
390+
alias = None
391+
if node.parent.type == 'variable_declarator':
392+
alias_node = node.parent.child_by_field_name('name')
393+
if alias_node:
394+
alias = self._get_node_text(alias_node)
395+
imports.append({'name': source, 'source': source, 'alias': alias, 'line_number': line_number, 'lang': self.language_name})
387396

388-
elif node.type == 'call_expression': # require
389-
args_node = node.child_by_field_name('arguments')
390-
if not args_node or args_node.named_child_count == 0: continue
391-
path_node = args_node.named_child(0)
392-
if not path_node or path_node.type != 'string': continue
393-
path = self._get_node_text(path_node).strip('\'"')
394-
395-
alias = None
396-
if node.parent.type == 'variable_declarator':
397-
alias_node = node.parent.child_by_field_name('name')
398-
if alias_node:
399-
alias = self._get_node_text(alias_node)
400-
401-
imports.append({"name": path, "full_import_name": path, "alias": alias if alias else path, "line_number": line_number, "context": None, "lang": self.language_name, "is_dependency": False})
402397
return imports
403398

404399
def _find_calls(self, root_node):

0 commit comments

Comments
 (0)