This is page 17 of 22. Use http://codebase.md/shashankss1205/codegraphcontext?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .cgcignore
├── .github
│ ├── FUNDING.yml
│ └── workflows
│ ├── e2e-tests.yml
│ ├── post_discord_invite.yml
│ ├── test.yml
│ └── update-contributors.yml
├── .gitignore
├── CLI_Commands.md
├── CONTRIBUTING.md
├── contributors.md
├── docs
│ ├── docs
│ │ ├── architecture.md
│ │ ├── cli.md
│ │ ├── contributing_languages.md
│ │ ├── contributing.md
│ │ ├── cookbook.md
│ │ ├── core.md
│ │ ├── future_work.md
│ │ ├── images
│ │ │ ├── 1.png
│ │ │ ├── 11.png
│ │ │ ├── 12.png
│ │ │ ├── 13.png
│ │ │ ├── 14.png
│ │ │ ├── 16.png
│ │ │ ├── 19.png
│ │ │ ├── 2.png
│ │ │ ├── 20.png
│ │ │ ├── 21.png
│ │ │ ├── 22.png
│ │ │ ├── 23.png
│ │ │ ├── 24.png
│ │ │ ├── 26.png
│ │ │ ├── 28.png
│ │ │ ├── 29.png
│ │ │ ├── 3.png
│ │ │ ├── 30.png
│ │ │ ├── 31.png
│ │ │ ├── 32.png
│ │ │ ├── 33.png
│ │ │ ├── 34.png
│ │ │ ├── 35.png
│ │ │ ├── 36.png
│ │ │ ├── 38.png
│ │ │ ├── 39.png
│ │ │ ├── 4.png
│ │ │ ├── 40.png
│ │ │ ├── 41.png
│ │ │ ├── 42.png
│ │ │ ├── 43.png
│ │ │ ├── 44.png
│ │ │ ├── 5.png
│ │ │ ├── 6.png
│ │ │ ├── 7.png
│ │ │ ├── 8.png
│ │ │ ├── 9.png
│ │ │ ├── Indexing.gif
│ │ │ ├── tool_images
│ │ │ │ ├── 1.png
│ │ │ │ ├── 2.png
│ │ │ │ └── 3.png
│ │ │ └── Usecase.gif
│ │ ├── index.md
│ │ ├── installation.md
│ │ ├── license.md
│ │ ├── server.md
│ │ ├── tools.md
│ │ ├── troubleshooting.md
│ │ ├── use_cases.md
│ │ └── watching.md
│ ├── mkdocs.yml
│ └── site
│ ├── 404.html
│ ├── architecture
│ │ └── index.html
│ ├── assets
│ │ ├── images
│ │ │ └── favicon.png
│ │ ├── javascripts
│ │ │ ├── bundle.79ae519e.min.js
│ │ │ ├── bundle.79ae519e.min.js.map
│ │ │ ├── lunr
│ │ │ │ ├── min
│ │ │ │ │ ├── lunr.ar.min.js
│ │ │ │ │ ├── lunr.da.min.js
│ │ │ │ │ ├── lunr.de.min.js
│ │ │ │ │ ├── lunr.du.min.js
│ │ │ │ │ ├── lunr.el.min.js
│ │ │ │ │ ├── lunr.es.min.js
│ │ │ │ │ ├── lunr.fi.min.js
│ │ │ │ │ ├── lunr.fr.min.js
│ │ │ │ │ ├── lunr.he.min.js
│ │ │ │ │ ├── lunr.hi.min.js
│ │ │ │ │ ├── lunr.hu.min.js
│ │ │ │ │ ├── lunr.hy.min.js
│ │ │ │ │ ├── lunr.it.min.js
│ │ │ │ │ ├── lunr.ja.min.js
│ │ │ │ │ ├── lunr.jp.min.js
│ │ │ │ │ ├── lunr.kn.min.js
│ │ │ │ │ ├── lunr.ko.min.js
│ │ │ │ │ ├── lunr.multi.min.js
│ │ │ │ │ ├── lunr.nl.min.js
│ │ │ │ │ ├── lunr.no.min.js
│ │ │ │ │ ├── lunr.pt.min.js
│ │ │ │ │ ├── lunr.ro.min.js
│ │ │ │ │ ├── lunr.ru.min.js
│ │ │ │ │ ├── lunr.sa.min.js
│ │ │ │ │ ├── lunr.stemmer.support.min.js
│ │ │ │ │ ├── lunr.sv.min.js
│ │ │ │ │ ├── lunr.ta.min.js
│ │ │ │ │ ├── lunr.te.min.js
│ │ │ │ │ ├── lunr.th.min.js
│ │ │ │ │ ├── lunr.tr.min.js
│ │ │ │ │ ├── lunr.vi.min.js
│ │ │ │ │ └── lunr.zh.min.js
│ │ │ │ ├── tinyseg.js
│ │ │ │ └── wordcut.js
│ │ │ └── workers
│ │ │ ├── search.2c215733.min.js
│ │ │ └── search.2c215733.min.js.map
│ │ └── stylesheets
│ │ ├── main.484c7ddc.min.css
│ │ ├── main.484c7ddc.min.css.map
│ │ ├── palette.ab4e12ef.min.css
│ │ └── palette.ab4e12ef.min.css.map
│ ├── cli
│ │ └── index.html
│ ├── contributing
│ │ └── index.html
│ ├── contributing_languages
│ │ └── index.html
│ ├── cookbook
│ │ └── index.html
│ ├── core
│ │ └── index.html
│ ├── future_work
│ │ └── index.html
│ ├── images
│ │ ├── 1.png
│ │ ├── 11.png
│ │ ├── 12.png
│ │ ├── 13.png
│ │ ├── 14.png
│ │ ├── 16.png
│ │ ├── 19.png
│ │ ├── 2.png
│ │ ├── 20.png
│ │ ├── 21.png
│ │ ├── 22.png
│ │ ├── 23.png
│ │ ├── 24.png
│ │ ├── 26.png
│ │ ├── 28.png
│ │ ├── 29.png
│ │ ├── 3.png
│ │ ├── 30.png
│ │ ├── 31.png
│ │ ├── 32.png
│ │ ├── 33.png
│ │ ├── 34.png
│ │ ├── 35.png
│ │ ├── 36.png
│ │ ├── 38.png
│ │ ├── 39.png
│ │ ├── 4.png
│ │ ├── 40.png
│ │ ├── 41.png
│ │ ├── 42.png
│ │ ├── 43.png
│ │ ├── 44.png
│ │ ├── 5.png
│ │ ├── 6.png
│ │ ├── 7.png
│ │ ├── 8.png
│ │ ├── 9.png
│ │ ├── Indexing.gif
│ │ ├── tool_images
│ │ │ ├── 1.png
│ │ │ ├── 2.png
│ │ │ └── 3.png
│ │ └── Usecase.gif
│ ├── index.html
│ ├── installation
│ │ └── index.html
│ ├── license
│ │ └── index.html
│ ├── search
│ │ └── search_index.json
│ ├── server
│ │ └── index.html
│ ├── sitemap.xml
│ ├── sitemap.xml.gz
│ ├── tools
│ │ └── index.html
│ ├── troubleshooting
│ │ └── index.html
│ ├── use_cases
│ │ └── index.html
│ └── watching
│ └── index.html
├── funding.json
├── images
│ ├── 1.png
│ ├── 11.png
│ ├── 12.png
│ ├── 13.png
│ ├── 14.png
│ ├── 16.png
│ ├── 19.png
│ ├── 2.png
│ ├── 20.png
│ ├── 21.png
│ ├── 22.png
│ ├── 23.png
│ ├── 24.png
│ ├── 26.png
│ ├── 28.png
│ ├── 29.png
│ ├── 3.png
│ ├── 30.png
│ ├── 31.png
│ ├── 32.png
│ ├── 33.png
│ ├── 34.png
│ ├── 35.png
│ ├── 36.png
│ ├── 38.png
│ ├── 39.png
│ ├── 4.png
│ ├── 40.png
│ ├── 41.png
│ ├── 42.png
│ ├── 43.png
│ ├── 44.png
│ ├── 5.png
│ ├── 6.png
│ ├── 7.png
│ ├── 8.png
│ ├── 9.png
│ ├── Indexing.gif
│ ├── tool_images
│ │ ├── 1.png
│ │ ├── 2.png
│ │ └── 3.png
│ └── Usecase.gif
├── LICENSE
├── MANIFEST.in
├── organizer
│ ├── CONTRIBUTING_LANGUAGES.md
│ ├── cookbook.md
│ ├── docs.md
│ ├── language_specific_nodes.md
│ ├── Tools_Exploration.md
│ └── troubleshoot.md
├── pyproject.toml
├── README.md
├── scripts
│ ├── generate_lang_contributors.py
│ ├── post_install_fix.sh
│ ├── test_all_parsers.py
│ └── update_language_parsers.py
├── SECURITY.md
├── src
│ └── codegraphcontext
│ ├── __init__.py
│ ├── __main__.py
│ ├── cli
│ │ ├── __init__.py
│ │ ├── cli_helpers.py
│ │ ├── config_manager.py
│ │ ├── main.py
│ │ ├── setup_macos.py
│ │ └── setup_wizard.py
│ ├── core
│ │ ├── __init__.py
│ │ ├── database_falkordb.py
│ │ ├── database.py
│ │ ├── falkor_worker.py
│ │ ├── jobs.py
│ │ └── watcher.py
│ ├── prompts.py
│ ├── server.py
│ ├── tools
│ │ ├── __init__.py
│ │ ├── advanced_language_query_tool.py
│ │ ├── code_finder.py
│ │ ├── graph_builder.py
│ │ ├── languages
│ │ │ ├── c.py
│ │ │ ├── cpp.py
│ │ │ ├── csharp.py
│ │ │ ├── go.py
│ │ │ ├── java.py
│ │ │ ├── javascript.py
│ │ │ ├── kotlin.py
│ │ │ ├── php.py
│ │ │ ├── python.py
│ │ │ ├── ruby.py
│ │ │ ├── rust.py
│ │ │ ├── scala.py
│ │ │ ├── swift.py
│ │ │ ├── typescript.py
│ │ │ └── typescriptjsx.py
│ │ ├── package_resolver.py
│ │ ├── query_tool_languages
│ │ │ ├── c_toolkit.py
│ │ │ ├── cpp_toolkit.py
│ │ │ ├── csharp_toolkit.py
│ │ │ ├── go_toolkit.py
│ │ │ ├── java_toolkit.py
│ │ │ ├── javascript_toolkit.py
│ │ │ ├── python_toolkit.py
│ │ │ ├── ruby_toolkit.py
│ │ │ ├── rust_toolkit.py
│ │ │ ├── scala_toolkit.py
│ │ │ ├── swift_toolkit.py
│ │ │ └── typescript_toolkit.py
│ │ └── system.py
│ └── utils
│ ├── debug_log.py
│ └── tree_sitter_manager.py
├── tests
│ ├── __init__.py
│ ├── conftest.py
│ ├── sample_project
│ │ ├── advanced_calls.py
│ │ ├── advanced_classes.py
│ │ ├── advanced_classes2.py
│ │ ├── advanced_functions.py
│ │ ├── advanced_imports.py
│ │ ├── async_features.py
│ │ ├── callbacks_decorators.py
│ │ ├── circular1.py
│ │ ├── circular2.py
│ │ ├── class_instantiation.py
│ │ ├── cli_and_dunder.py
│ │ ├── complex_classes.py
│ │ ├── comprehensions_generators.py
│ │ ├── context_managers.py
│ │ ├── control_flow.py
│ │ ├── datatypes.py
│ │ ├── dynamic_dispatch.py
│ │ ├── dynamic_imports.py
│ │ ├── edge_cases
│ │ │ ├── comments_only.py
│ │ │ ├── docstring_only.py
│ │ │ ├── empty.py
│ │ │ ├── hardcoded_secrets.py
│ │ │ ├── long_functions.py
│ │ │ └── syntax_error.py
│ │ ├── function_chains.py
│ │ ├── generators.py
│ │ ├── import_reexports.py
│ │ ├── mapping_calls.py
│ │ ├── module_a.py
│ │ ├── module_b.py
│ │ ├── module_c
│ │ │ ├── __init__.py
│ │ │ ├── submodule1.py
│ │ │ └── submodule2.py
│ │ ├── namespace_pkg
│ │ │ └── ns_module.py
│ │ ├── pattern_matching.py
│ │ └── typing_examples.py
│ ├── sample_project_c
│ │ ├── cgc_sample
│ │ ├── include
│ │ │ ├── config.h
│ │ │ ├── math
│ │ │ │ └── vec.h
│ │ │ ├── module.h
│ │ │ ├── platform.h
│ │ │ └── util.h
│ │ ├── Makefile
│ │ ├── README.md
│ │ └── src
│ │ ├── main.c
│ │ ├── math
│ │ │ └── vec.c
│ │ ├── module.c
│ │ └── util.c
│ ├── sample_project_cpp
│ │ ├── class_features.cpp
│ │ ├── classes.cpp
│ │ ├── control_flow.cpp
│ │ ├── edge_cases.cpp
│ │ ├── enum_struct_union.cpp
│ │ ├── exceptions.cpp
│ │ ├── file_io.cpp
│ │ ├── function_chain.cpp
│ │ ├── function_chain.h
│ │ ├── function_types.cpp
│ │ ├── main.cpp
│ │ ├── main.exe
│ │ ├── namespaces.cpp
│ │ ├── raii_example.cpp
│ │ ├── README.md
│ │ ├── sample_project.exe
│ │ ├── stl_usage.cpp
│ │ ├── templates.cpp
│ │ └── types_variable_assignments.cpp
│ ├── sample_project_csharp
│ │ ├── README.md
│ │ └── src
│ │ └── Example.App
│ │ ├── Attributes
│ │ │ └── CustomAttributes.cs
│ │ ├── Example.App.csproj
│ │ ├── Models
│ │ │ ├── Person.cs
│ │ │ ├── Point.cs
│ │ │ ├── Role.cs
│ │ │ └── User.cs
│ │ ├── OuterClass.cs
│ │ ├── Program.cs
│ │ ├── Services
│ │ │ ├── GreetingService.cs
│ │ │ ├── IGreetingService.cs
│ │ │ └── LegacyService.cs
│ │ └── Utils
│ │ ├── CollectionHelper.cs
│ │ └── FileHelper.cs
│ ├── sample_project_go
│ │ ├── advanced_types.go
│ │ ├── basic_functions.go
│ │ ├── embedded_composition.go
│ │ ├── error_handling.go
│ │ ├── generics.go
│ │ ├── go.mod
│ │ ├── goroutines_channels.go
│ │ ├── interfaces.go
│ │ ├── packages_imports.go
│ │ ├── README.md
│ │ ├── structs_methods.go
│ │ └── util
│ │ └── helpers.go
│ ├── sample_project_java
│ │ ├── out
│ │ │ └── com
│ │ │ └── example
│ │ │ └── app
│ │ │ ├── annotations
│ │ │ │ └── Logged.class
│ │ │ ├── Main.class
│ │ │ ├── misc
│ │ │ │ ├── Outer.class
│ │ │ │ └── Outer$Inner.class
│ │ │ ├── model
│ │ │ │ ├── Role.class
│ │ │ │ └── User.class
│ │ │ ├── service
│ │ │ │ ├── AbstractGreeter.class
│ │ │ │ ├── GreetingService.class
│ │ │ │ └── impl
│ │ │ │ └── GreetingServiceImpl.class
│ │ │ └── util
│ │ │ ├── CollectionUtils.class
│ │ │ └── IOHelper.class
│ │ ├── README.md
│ │ ├── sources.txt
│ │ └── src
│ │ └── com
│ │ └── example
│ │ └── app
│ │ ├── annotations
│ │ │ └── Logged.java
│ │ ├── Main.java
│ │ ├── misc
│ │ │ └── Outer.java
│ │ ├── model
│ │ │ ├── Role.java
│ │ │ └── User.java
│ │ ├── service
│ │ │ ├── AbstractGreeter.java
│ │ │ ├── GreetingService.java
│ │ │ └── impl
│ │ │ └── GreetingServiceImpl.java
│ │ └── util
│ │ ├── CollectionUtils.java
│ │ └── IOHelper.java
│ ├── sample_project_javascript
│ │ ├── arrays.js
│ │ ├── asyncAwait.js
│ │ ├── classes.js
│ │ ├── dom.js
│ │ ├── errorHandling.js
│ │ ├── events.js
│ │ ├── exporter.js
│ │ ├── fetchAPI.js
│ │ ├── fixtures
│ │ │ └── js
│ │ │ └── accessors.js
│ │ ├── functions.js
│ │ ├── importer.js
│ │ ├── objects.js
│ │ ├── promises.js
│ │ ├── README.md
│ │ └── variables.js
│ ├── sample_project_kotlin
│ │ ├── AdvancedClasses.kt
│ │ ├── Annotations.kt
│ │ ├── Coroutines.kt
│ │ ├── EdgeCases.kt
│ │ ├── Functions.kt
│ │ ├── Main.kt
│ │ ├── Properties.kt
│ │ └── User.kt
│ ├── sample_project_misc
│ │ ├── index.html
│ │ ├── README.md
│ │ ├── styles.css
│ │ ├── tables.css
│ │ └── tables.html
│ ├── sample_project_php
│ │ ├── classes_objects.php
│ │ ├── database.php
│ │ ├── edgecases.php
│ │ ├── error_handling.php
│ │ ├── file_handling.php
│ │ ├── functions.php
│ │ ├── generators_iterators.php
│ │ ├── globals_superglobals.php
│ │ ├── Inheritance.php
│ │ ├── interface_traits.php
│ │ └── README.md
│ ├── sample_project_ruby
│ │ ├── class_example.rb
│ │ ├── enumerables.rb
│ │ ├── error_handling.rb
│ │ ├── file_io.rb
│ │ ├── inheritance_example.rb
│ │ ├── main.rb
│ │ ├── metaprogramming.rb
│ │ ├── mixins_example.rb
│ │ ├── module_example.rb
│ │ └── tests
│ │ ├── test_mixins.py
│ │ └── test_sample.rb
│ ├── sample_project_rust
│ │ ├── Cargo.toml
│ │ ├── README.md
│ │ └── src
│ │ ├── basic_functions.rs
│ │ ├── concurrency.rs
│ │ ├── error_handling.rs
│ │ ├── generics.rs
│ │ ├── iterators_closures.rs
│ │ ├── lib.rs
│ │ ├── lifetimes_references.rs
│ │ ├── modules.rs
│ │ ├── smart_pointers.rs
│ │ ├── structs_enums.rs
│ │ └── traits.rs
│ ├── sample_project_scala
│ │ ├── Animals.scala
│ │ ├── Complex.scala
│ │ ├── Functional.scala
│ │ ├── Geometry.scala
│ │ ├── Main.scala
│ │ ├── PackageObject.scala
│ │ ├── Script.sc
│ │ ├── Services.scala
│ │ ├── Shapes.scala
│ │ ├── Utils.scala
│ │ └── Variables.scala
│ ├── sample_project_swift
│ │ ├── Generics.swift
│ │ ├── Main.swift
│ │ ├── README.md
│ │ ├── Shapes.swift
│ │ ├── User.swift
│ │ └── Vehicles.swift
│ ├── sample_project_typescript
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── sample_tsx.tsx
│ │ ├── src
│ │ │ ├── advanced-types.ts
│ │ │ ├── async-promises.ts
│ │ │ ├── classes-inheritance.ts
│ │ │ ├── decorators-metadata.ts
│ │ │ ├── error-validation.ts
│ │ │ ├── functions-generics.ts
│ │ │ ├── index.ts
│ │ │ ├── modules-namespaces.ts
│ │ │ ├── types-interfaces.ts
│ │ │ └── utilities-helpers.ts
│ │ └── tsconfig.json
│ ├── test_cpp_parser.py
│ ├── test_database_validation.py
│ ├── test_end_to_end.py
│ ├── test_graph_indexing_js.py
│ ├── test_graph_indexing.py
│ ├── test_kotlin_parser.py
│ ├── test_swift_parser.py
│ ├── test_tree_sitter
│ │ ├── __init__.py
│ │ ├── class_instantiation.py
│ │ ├── complex_classes.py
│ │ └── test_file.py
│ ├── test_tree_sitter_manager.py
│ └── test_typescript_parser.py
├── visualize_graph.py
├── website
│ ├── .example.env
│ ├── .gitignore
│ ├── api
│ │ └── pypi.ts
│ ├── bun.lockb
│ ├── components.json
│ ├── eslint.config.js
│ ├── index.html
│ ├── package-lock.json
│ ├── package.json
│ ├── postcss.config.js
│ ├── public
│ │ ├── favicon.ico
│ │ ├── placeholder.svg
│ │ └── robots.txt
│ ├── README.md
│ ├── src
│ │ ├── App.css
│ │ ├── App.tsx
│ │ ├── assets
│ │ │ ├── function-calls.png
│ │ │ ├── graph-total.png
│ │ │ ├── hero-graph.jpg
│ │ │ └── hierarchy.png
│ │ ├── components
│ │ │ ├── ComparisonTable.tsx
│ │ │ ├── CookbookSection.tsx
│ │ │ ├── DemoSection.tsx
│ │ │ ├── ExamplesSection.tsx
│ │ │ ├── FeaturesSection.tsx
│ │ │ ├── Footer.tsx
│ │ │ ├── HeroSection.tsx
│ │ │ ├── InstallationSection.tsx
│ │ │ ├── MoveToTop.tsx
│ │ │ ├── ShowDownloads.tsx
│ │ │ ├── ShowStarGraph.tsx
│ │ │ ├── SocialMentionsTimeline.tsx
│ │ │ ├── TestimonialSection.tsx
│ │ │ ├── ThemeProvider.tsx
│ │ │ ├── ThemeToggle.tsx
│ │ │ └── ui
│ │ │ ├── accordion.tsx
│ │ │ ├── alert-dialog.tsx
│ │ │ ├── alert.tsx
│ │ │ ├── aspect-ratio.tsx
│ │ │ ├── avatar.tsx
│ │ │ ├── badge.tsx
│ │ │ ├── breadcrumb.tsx
│ │ │ ├── button.tsx
│ │ │ ├── calendar.tsx
│ │ │ ├── card.tsx
│ │ │ ├── carousel.tsx
│ │ │ ├── chart.tsx
│ │ │ ├── checkbox.tsx
│ │ │ ├── collapsible.tsx
│ │ │ ├── command.tsx
│ │ │ ├── context-menu.tsx
│ │ │ ├── dialog.tsx
│ │ │ ├── drawer.tsx
│ │ │ ├── dropdown-menu.tsx
│ │ │ ├── form.tsx
│ │ │ ├── hover-card.tsx
│ │ │ ├── input-otp.tsx
│ │ │ ├── input.tsx
│ │ │ ├── label.tsx
│ │ │ ├── menubar.tsx
│ │ │ ├── navigation-menu.tsx
│ │ │ ├── orbiting-circles.tsx
│ │ │ ├── pagination.tsx
│ │ │ ├── popover.tsx
│ │ │ ├── progress.tsx
│ │ │ ├── radio-group.tsx
│ │ │ ├── resizable.tsx
│ │ │ ├── scroll-area.tsx
│ │ │ ├── select.tsx
│ │ │ ├── separator.tsx
│ │ │ ├── sheet.tsx
│ │ │ ├── sidebar.tsx
│ │ │ ├── skeleton.tsx
│ │ │ ├── slider.tsx
│ │ │ ├── sonner.tsx
│ │ │ ├── switch.tsx
│ │ │ ├── table.tsx
│ │ │ ├── tabs.tsx
│ │ │ ├── textarea.tsx
│ │ │ ├── toast.tsx
│ │ │ ├── toaster.tsx
│ │ │ ├── toggle-group.tsx
│ │ │ ├── toggle.tsx
│ │ │ ├── tooltip.tsx
│ │ │ └── use-toast.ts
│ │ ├── hooks
│ │ │ ├── use-mobile.tsx
│ │ │ └── use-toast.ts
│ │ ├── index.css
│ │ ├── lib
│ │ │ └── utils.ts
│ │ ├── main.tsx
│ │ ├── pages
│ │ │ ├── Index.tsx
│ │ │ └── NotFound.tsx
│ │ └── vite-env.d.ts
│ ├── tailwind.config.ts
│ ├── tsconfig.app.json
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ ├── vercel.json
│ └── vite.config.ts
└── windows_setup_guide.md
```
# Files
--------------------------------------------------------------------------------
/tests/test_graph_indexing.py:
--------------------------------------------------------------------------------
```python
1 |
2 | import pytest
3 | import os
4 |
5 | from .conftest import SAMPLE_PROJECT_PATH
6 |
7 | # ==============================================================================
8 | # == EXPECTED RELATIONSHIPS
9 | # ==============================================================================
10 |
11 | EXPECTED_STRUCTURE = [
12 | ("module_a.py", "foo", "Function"),
13 | ("module_a.py", "bar", "Function"),
14 | ("module_a.py", "outer", "Function"),
15 | ("module_b.py", "helper", "Function"),
16 | ("module_b.py", "process_data", "Function"),
17 | ("module_b.py", "factorial", "Function"),
18 | ("advanced_classes.py", "A", "Class"),
19 | ("advanced_classes.py", "B", "Class"),
20 | ("advanced_classes.py", "C", "Class"),
21 | ("module_a.py", "nested", "Function"),
22 | ("advanced_calls.py", "square", "Function"),
23 | ("advanced_calls.py", "calls", "Function"),
24 | ("advanced_calls.py", "Dummy", "Class"),
25 | ("advanced_classes2.py", "Base", "Class"),
26 | ("advanced_classes2.py", "Mid", "Class"),
27 | ("advanced_classes2.py", "Final", "Class"),
28 | ("advanced_classes2.py", "Mixin1", "Class"),
29 | ("advanced_classes2.py", "Mixin2", "Class"),
30 | ("advanced_classes2.py", "Combined", "Class"),
31 | ("advanced_classes2.py", "Point", "Class"),
32 | ("advanced_classes2.py", "Color", "Class"),
33 | ("advanced_classes2.py", "handle", "Function"),
34 | ("advanced_functions.py", "with_defaults", "Function"),
35 | ("advanced_functions.py", "with_args_kwargs", "Function"),
36 | ("advanced_functions.py", "higher_order", "Function"),
37 | ("advanced_functions.py", "return_function", "Function"),
38 | ("advanced_imports.py", "outer_import", "Function"),
39 | ("advanced_imports.py", "use_random", "Function"),
40 | ("async_features.py", "fetch_data", "Function"),
41 | ("async_features.py", "main", "Function"),
42 | ("callbacks_decorators.py", "executor", "Function"),
43 | ("callbacks_decorators.py", "square", "Function"),
44 | ("callbacks_decorators.py", "log_decorator", "Function"),
45 | ("callbacks_decorators.py", "hello", "Function"),
46 | ("class_instantiation.py", "A", "Class"),
47 | ("class_instantiation.py", "B", "Class"),
48 | ("class_instantiation.py", "Fluent", "Class"),
49 | ("function_chains.py", "f1", "Function"),
50 | ("function_chains.py", "f2", "Function"),
51 | ("function_chains.py", "f3", "Function"),
52 | ("function_chains.py", "make_adder", "Function"),
53 | ("generators.py", "gen_numbers", "Function"),
54 | ("generators.py", "agen_numbers", "Function"),
55 | ("generators.py", "async_with_example", "Function"),
56 | ("generators.py", "AsyncCM", "Class"),
57 | ("datatypes.py", "Point", "Class"),
58 | ("datatypes.py", "Color", "Class"),
59 | ("complex_classes.py", "Base", "Class"),
60 | ("complex_classes.py", "Child", "Class"),
61 | ("complex_classes.py", "decorator", "Function"),
62 | ("complex_classes.py", "decorated_function", "Function"),
63 | ("control_flow.py", "choose_path", "Function"),
64 | ("control_flow.py", "ternary", "Function"),
65 | ("control_flow.py", "try_except_finally", "Function"),
66 | ("control_flow.py", "conditional_inner_import", "Function"),
67 | ("control_flow.py", "env_based_import", "Function"),
68 | ("circular1.py", "func1", "Function"),
69 | ("circular2.py", "func2", "Function"),
70 | ("cli_and_dunder.py", "run", "Function"),
71 | ("comprehensions_generators.py", "double", "Function"),
72 | ("context_managers.py", "FileOpener", "Class"),
73 | ("context_managers.py", "use_file", "Function"),
74 | ("dynamic_dispatch.py", "add", "Function"),
75 | ("dynamic_dispatch.py", "sub", "Function"),
76 | ("dynamic_dispatch.py", "mul", "Function"),
77 | ("dynamic_dispatch.py", "dispatch_by_key", "Function"),
78 | ("dynamic_dispatch.py", "dispatch_by_string", "Function"),
79 | ("dynamic_dispatch.py", "partial_example", "Function"),
80 | ("dynamic_dispatch.py", "C", "Class"),
81 | ("dynamic_dispatch.py", "methodcaller_example", "Function"),
82 | ("dynamic_dispatch.py", "dynamic_import_call", "Function"),
83 | ("dynamic_imports.py", "import_optional", "Function"),
84 | ("dynamic_imports.py", "import_by___import__", "Function"),
85 | ("dynamic_imports.py", "importlib_runtime", "Function"),
86 | ("import_reexports.py", "core_function", "Function"),
87 | ("import_reexports.py", "reexport", "Function"),
88 | ("mapping_calls.py", "Dispatcher", "Class"),
89 | ("mapping_calls.py", "use_dispatcher", "Function"),
90 | ("module_c/submodule1.py", "call_helper_twice", "Function"),
91 | ("module_c/submodule2.py", "wrapper", "Function"),
92 | ("namespace_pkg/ns_module.py", "ns_func", "Function"),
93 | ("pattern_matching.py", "matcher", "Function"),
94 | ("typing_examples.py", "typed_func", "Function"),
95 | ("typing_examples.py", "union_func", "Function"),
96 | ("typing_examples.py", "dict_func", "Function"),
97 | ]
98 |
99 | EXPECTED_INHERITANCE = [
100 | pytest.param("C", "advanced_classes.py", "A", "advanced_classes.py", id="C inherits from A"),
101 | pytest.param("C", "advanced_classes.py", "B", "advanced_classes.py", id="C inherits from B"),
102 | pytest.param("ConcreteThing", "advanced_classes.py", "AbstractThing", "advanced_classes.py", id="ConcreteThing inherits from AbstractThing"),
103 | pytest.param("Mid", "advanced_classes2.py", "Base", "advanced_classes2.py", id="Mid inherits from Base"),
104 | pytest.param("Final", "advanced_classes2.py", "Mid", "advanced_classes2.py", id="Final inherits from Mid"),
105 | pytest.param("Combined", "advanced_classes2.py", "Mixin1", "advanced_classes2.py", id="Combined inherits from Mixin1"),
106 | pytest.param("Combined", "advanced_classes2.py", "Mixin2", "advanced_classes2.py", id="Combined inherits from Mixin2"),
107 | pytest.param("B", "class_instantiation.py", "A", "class_instantiation.py", id="B inherits from A"),
108 | pytest.param("B", "class_instantiation.py", "A", "class_instantiation.py", marks=pytest.mark.skip(reason="Indexer does not support inheritance via super() calls"), id="B inherits from A via super()"),
109 | pytest.param("Child", "complex_classes.py", "Base", "complex_classes.py", id="Child inherits from Base"),
110 | ]
111 |
112 | EXPECTED_CALLS = [
113 | pytest.param("foo", "module_a.py", None, "helper", "module_b.py", None, id="module_a.foo->module_b.helper"),
114 | pytest.param("foo", "module_a.py", None, "process_data", "module_b.py", None, id="module_a.foo->module_b.process_data"),
115 | pytest.param("factorial", "module_b.py", None, "factorial", "module_b.py", None, id="module_b.factorial->recursive"),
116 | pytest.param("calls", "advanced_calls.py", None, "square", "advanced_calls.py", None, id="advanced_calls.calls->square"),
117 | pytest.param("call_helper_twice", "module_c/submodule1.py", None, "helper", "module_b.py", None, id="submodule1.call_helper_twice->module_b.helper"),
118 | pytest.param("wrapper", "module_c/submodule2.py", None, "call_helper_twice", "module_c/submodule1.py", None, id="submodule2.wrapper->submodule1.call_helper_twice"),
119 | pytest.param("main", "async_features.py", None, "fetch_data", "async_features.py", None, id="async.main->fetch_data"),
120 | pytest.param("func1", "circular1.py", None, "func2", "circular2.py", None, id="circular1.func1->circular2.func2"),
121 | pytest.param("run", "cli_and_dunder.py", None, "with_defaults", "advanced_functions.py", None, id="cli.run->with_defaults"),
122 | pytest.param("use_dispatcher", "mapping_calls.py", None, "call", "mapping_calls.py", None, id="mapping.use_dispatcher->call"),
123 | pytest.param("calls", "advanced_calls.py", None, "method", "advanced_calls.py", "Dummy", marks=pytest.mark.skip(reason="Dynamic call with getattr is not supported"), id="advanced_calls.calls->Dummy.method"),
124 | pytest.param("both", "advanced_classes2.py", "Combined", "m1", "advanced_classes2.py", "Mixin1", id="advanced_classes2.both->m1"),
125 | pytest.param("both", "advanced_classes2.py", "Combined", "m2", "advanced_classes2.py", "Mixin2", id="advanced_classes2.both->m2"),
126 | pytest.param("executor", "callbacks_decorators.py", None, "square", "callbacks_decorators.py", None, marks=pytest.mark.skip(reason="Dynamic call passing function as argument is not supported"), id="callbacks.executor->square"),
127 | pytest.param("reexport", "import_reexports.py", None, "core_function", "import_reexports.py", None, id="reexport->core_function"),
128 | pytest.param("greet", "class_instantiation.py", "B", "greet", "class_instantiation.py", "A", marks=pytest.mark.skip(reason="super() calls are not supported yet"), id="B.greet->A.greet"),
129 | pytest.param("greet", "complex_classes.py", "Child", "greet", "complex_classes.py", "Base", marks=pytest.mark.skip(reason="super() calls are not supported yet"), id="Child.greet->Base.greet"),
130 | pytest.param("class_method", "complex_classes.py", "Child", "greet", "complex_classes.py", "Child", id="Child.class_method->Child.greet"),
131 | pytest.param("use_file", "context_managers.py", None, "__enter__", "context_managers.py", "FileOpener", marks=pytest.mark.skip(reason="Implicit context manager calls not supported"), id="use_file->FileOpener.__enter__"),
132 | pytest.param("partial_example", "dynamic_dispatch.py", None, "add", "dynamic_dispatch.py", None, marks=pytest.mark.skip(reason="Calls via functools.partial not supported yet"), id="partial_example->add"),
133 | pytest.param("async_with_example", "generators.py", None, "__aenter__", "generators.py", "AsyncCM", marks=pytest.mark.skip(reason="Implicit async context manager calls not supported"), id="async_with_example->AsyncCM.__aenter__"),
134 | pytest.param("call", "mapping_calls.py", "Dispatcher", "start", "mapping_calls.py", "Dispatcher", marks=pytest.mark.skip(reason="Dynamic call via dict lookup not supported"), id="Dispatcher.call->start"),
135 | pytest.param("greet", "class_instantiation.py", None, "greet", "class_instantiation.py", "A", marks=pytest.mark.skip(reason="Indexer does not capture calls to methods of instantiated objects within the same file"), id="A.greet called"),
136 | pytest.param("greet", "class_instantiation.py", None, "greet", "class_instantiation.py", "B", marks=pytest.mark.skip(reason="Indexer does not capture calls to methods of instantiated objects within the same file"), id="B.greet called"),
137 | pytest.param("step1", "class_instantiation.py", "Fluent", "step1", "class_instantiation.py", "Fluent", marks=pytest.mark.skip(reason="Indexer does not capture method chaining calls"), id="Fluent.step1 called"),
138 | pytest.param("step2", "class_instantiation.py", "Fluent", "step2", "class_instantiation.py", "Fluent", marks=pytest.mark.skip(reason="Indexer does not capture method chaining calls"), id="Fluent.step2 called"),
139 | pytest.param("step3", "class_instantiation.py", "Fluent", "step3", "class_instantiation.py", "Fluent", marks=pytest.mark.skip(reason="Indexer does not capture method chaining calls"), id="Fluent.step3 called"),
140 | pytest.param("dynamic", "class_instantiation.py", "B", "lambda", "class_instantiation.py", None, marks=pytest.mark.skip(reason="Dynamic attribute assignment and lambda calls not supported"), id="B.dynamic called"),
141 | pytest.param("add_argument", "cli_and_dunder.py", None, "add_argument", None, None, marks=pytest.mark.skip(reason="Calls to external library methods not fully supported"), id="ArgumentParser.add_argument called"),
142 | pytest.param("parse_args", "cli_and_dunder.py", None, "parse_args", None, None, marks=pytest.mark.skip(reason="Calls to external library methods not fully supported"), id="ArgumentParser.parse_args called"),
143 | pytest.param("print", "cli_and_dunder.py", None, "print", None, None, marks=pytest.mark.skip(reason="Built-in function calls not explicitly indexed"), id="print called"),
144 | pytest.param("double", "comprehensions_generators.py", None, "double", "comprehensions_generators.py", None, marks=pytest.mark.skip(reason="Indexer does not capture calls within comprehensions/generators"), id="double called in list comprehension"),
145 | pytest.param("range", "comprehensions_generators.py", None, "range", None, None, marks=pytest.mark.skip(reason="Built-in function calls not explicitly indexed"), id="range called in list comprehension"),
146 | pytest.param("double", "comprehensions_generators.py", None, "double", "comprehensions_generators.py", None, marks=pytest.mark.skip(reason="Indexer does not capture calls within comprehensions/generators"), id="double called in generator expression"),
147 | pytest.param("range", "comprehensions_generators.py", None, "range", None, None, marks=pytest.mark.skip(reason="Built-in function calls not explicitly indexed"), id="range called in generator expression"),
148 | pytest.param("list", "comprehensions_generators.py", None, "list", None, None, marks=pytest.mark.skip(reason="Built-in function calls not explicitly indexed"), id="list called"),
149 | pytest.param("sorted", "comprehensions_generators.py", None, "sorted", None, None, marks=pytest.mark.skip(reason="Built-in function calls not explicitly indexed"), id="sorted called"),
150 | pytest.param("len", "comprehensions_generators.py", None, "len", None, None, marks=pytest.mark.skip(reason="Built-in function calls not explicitly indexed"), id="len called"),
151 | pytest.param("open", "comprehensions_generators.py", None, "open", None, None, marks=pytest.mark.skip(reason="Built-in function calls not explicitly indexed"), id="open called for write"),
152 | pytest.param("write", "comprehensions_generators.py", None, "write", None, None, marks=pytest.mark.skip(reason="Method calls on built-in types not explicitly indexed"), id="write called"),
153 | pytest.param("open", "comprehensions_generators.py", None, "open", None, None, marks=pytest.mark.skip(reason="Built-in function calls not explicitly indexed"), id="open called for read"),
154 | pytest.param("read", "comprehensions_generators.py", None, "read", None, None, marks=pytest.mark.skip(reason="Method calls on built-in types not explicitly indexed"), id="read called"),
155 | pytest.param("ValueError", "control_flow.py", None, "ValueError", None, None, marks=pytest.mark.skip(reason="Built-in exception constructors not explicitly indexed"), id="ValueError called"),
156 | pytest.param("str", "control_flow.py", None, "str", None, None, marks=pytest.mark.skip(reason="Built-in type constructors not explicitly indexed"), id="str called"),
157 | pytest.param("getenv", "control_flow.py", None, "getenv", None, None, marks=pytest.mark.skip(reason="Calls to external library methods not fully supported"), id="os.getenv called"),
158 | pytest.param("dumps", "control_flow.py", None, "dumps", None, None, marks=pytest.mark.skip(reason="Calls to external library methods not fully supported"), id="json.dumps called"),
159 | pytest.param("namedtuple", "datatypes.py", None, "namedtuple", None, None, marks=pytest.mark.skip(reason="Calls to external library functions not fully supported"), id="namedtuple called"),
160 | pytest.param("DISPATCH", "dynamic_dispatch.py", None, "add", "dynamic_dispatch.py", None, marks=pytest.mark.skip(reason="Dynamic dispatch via dictionary lookup not supported"), id="dispatch_by_key calls add dynamically"),
161 | pytest.param("DISPATCH", "dynamic_dispatch.py", None, "sub", "dynamic_dispatch.py", None, marks=pytest.mark.skip(reason="Dynamic dispatch via dictionary lookup not supported"), id="dispatch_by_key calls sub dynamically"),
162 | pytest.param("DISPATCH", "dynamic_dispatch.py", None, "mul", "dynamic_dispatch.py", None, marks=pytest.mark.skip(reason="Dynamic dispatch via dictionary lookup not supported"), id="dispatch_by_key calls mul dynamically"),
163 | pytest.param("get", "dynamic_dispatch.py", None, "get", None, None, marks=pytest.mark.skip(reason="Calls to built-in dictionary methods not explicitly indexed"), id="globals().get called"),
164 | pytest.param("callable", "dynamic_dispatch.py", None, "callable", None, None, marks=pytest.mark.skip(reason="Built-in function calls not explicitly indexed"), id="callable called"),
165 | pytest.param("partial", "dynamic_dispatch.py", None, "partial", None, None, marks=pytest.mark.skip(reason="Calls to external library functions not fully supported"), id="partial called"),
166 | pytest.param("methodcaller", "dynamic_dispatch.py", None, "methodcaller", None, None, marks=pytest.mark.skip(reason="Calls to external library functions not fully supported"), id="methodcaller called"),
167 | pytest.param("method", "dynamic_dispatch.py", "C", "method", "dynamic_dispatch.py", "C", marks=pytest.mark.skip(reason="Dynamic call via operator.methodcaller not supported"), id="C.method called via methodcaller"),
168 | pytest.param("import_module", "dynamic_dispatch.py", None, "import_module", None, None, marks=pytest.mark.skip(reason="Calls to external library functions not fully supported"), id="importlib.import_module called"),
169 | pytest.param("getattr", "dynamic_dispatch.py", None, "getattr", None, None, marks=pytest.mark.skip(reason="Built-in function calls not explicitly indexed"), id="getattr called"),
170 | pytest.param("dumps", "dynamic_imports.py", None, "dumps", None, None, marks=pytest.mark.skip(reason="Calls to external library methods not fully supported"), id="json.dumps called in import_optional"),
171 | pytest.param("__import__", "dynamic_imports.py", None, "__import__", None, None, marks=pytest.mark.skip(reason="Built-in function calls not explicitly indexed"), id="__import__ called"),
172 | pytest.param("getattr", "dynamic_imports.py", None, "getattr", None, None, marks=pytest.mark.skip(reason="Built-in function calls not explicitly indexed"), id="getattr called in import_by___import__"),
173 | pytest.param("import_module", "dynamic_imports.py", None, "import_module", None, None, marks=pytest.mark.skip(reason="Calls to external library functions not fully supported"), id="importlib.import_module called in importlib_runtime"),
174 | pytest.param("getattr", "dynamic_imports.py", None, "getattr", None, None, marks=pytest.mark.skip(reason="Built-in function calls not explicitly indexed"), id="getattr called in importlib_runtime"),
175 | pytest.param("f3", "function_chains.py", None, "f3", "function_chains.py", None, marks=pytest.mark.skip(reason="Indexer does not capture chained function calls"), id="f3 called"),
176 | pytest.param("f2", "function_chains.py", None, "f2", "function_chains.py", None, marks=pytest.mark.skip(reason="Indexer does not capture chained function calls"), id="f2 called"),
177 | pytest.param("f1", "function_chains.py", None, "f1", "function_chains.py", None, marks=pytest.mark.skip(reason="Indexer does not capture chained function calls"), id="f1 called"),
178 | pytest.param("strip", "function_chains.py", None, "strip", None, None, marks=pytest.mark.skip(reason="Method calls on built-in types not explicitly indexed"), id="strip called"),
179 | pytest.param("lower", "function_chains.py", None, "lower", None, None, marks=pytest.mark.skip(reason="Method calls on built-in types not explicitly indexed"), id="lower called"),
180 | pytest.param("replace", "function_chains.py", None, "replace", None, None, marks=pytest.mark.skip(reason="Method calls on built-in types not explicitly indexed"), id="replace called"),
181 | pytest.param("make_adder", "function_chains.py", None, "make_adder", "function_chains.py", None, marks=pytest.mark.skip(reason="Indexer does not capture calls to functions that return functions"), id="make_adder called"),
182 | pytest.param("adder", "function_chains.py", None, "adder", "function_chains.py", None, marks=pytest.mark.skip(reason="Indexer does not capture calls to inner functions returned by other functions"), id="adder called"),
183 | pytest.param("make_adder", "function_chains.py", None, "make_adder", "function_chains.py", None, marks=pytest.mark.skip(reason="Indexer does not capture calls to functions that return functions in a chain"), id="make_adder called in chain"),
184 | pytest.param("adder", "function_chains.py", None, "adder", "function_chains.py", None, marks=pytest.mark.skip(reason="Indexer does not capture calls to inner functions returned by other functions in a chain"), id="adder called in chain"),
185 | pytest.param("sqrt", "import_reexports.py", None, "sqrt", None, None, marks=pytest.mark.skip(reason="Calls to aliased imported functions not fully supported"), id="m.sqrt called"),
186 | pytest.param("Dispatcher", "mapping_calls.py", None, "Dispatcher", "mapping_calls.py", None, marks=pytest.mark.skip(reason="Indexer does not capture class constructor calls"), id="Dispatcher constructor called"),
187 | pytest.param("str", "pattern_matching.py", None, "str", None, None, marks=pytest.mark.skip(reason="Built-in type constructors not explicitly indexed"), id="str called in pattern matching"),
188 | ]
189 |
190 | EXPECTED_IMPORTS = [
191 | pytest.param("module_a.py", "math", id="module_a imports math"),
192 | pytest.param("module_a.py", "module_b", id="module_a imports module_b"),
193 | pytest.param("advanced_imports.py", "math", id="advanced_imports imports math"),
194 | pytest.param("advanced_imports.py", "random", id="advanced_imports imports random"),
195 | pytest.param("advanced_imports.py", "sys", id="advanced_imports imports sys"),
196 | pytest.param("async_features.py", "asyncio", id="async_features imports asyncio"),
197 | pytest.param("circular1.py", "func2", id="circular1 imports func2"),
198 | pytest.param("circular2.py", "func1", id="circular2 imports func1"),
199 | pytest.param("cli_and_dunder.py", "argparse", id="cli_and_dunder imports argparse"),
200 | pytest.param("cli_and_dunder.py", "advanced_functions", id="cli_and_dunder imports advanced_functions"),
201 | pytest.param("control_flow.py", "os", id="control_flow imports os"),
202 | pytest.param("datatypes.py", "dataclass", id="datatypes imports dataclass"),
203 | pytest.param("datatypes.py", "enum", id="datatypes imports enum"),
204 | pytest.param("datatypes.py", "namedtuple", id="datatypes imports namedtuple"),
205 | pytest.param("dynamic_dispatch.py", "partial", id="dynamic_dispatch imports partial"),
206 | pytest.param("dynamic_dispatch.py", "operator", id="dynamic_dispatch imports operator"),
207 | pytest.param("dynamic_dispatch.py", "importlib", id="dynamic_dispatch imports importlib"),
208 | pytest.param("dynamic_imports.py", "importlib", id="dynamic_imports imports importlib"),
209 | pytest.param("import_reexports.py", "math", marks=pytest.mark.skip(reason="Indexer does not support aliased imports (e.g., 'import math as m')"), id="import_reexports imports math"),
210 | pytest.param("module_c/submodule1.py", "helper", id="submodule1 imports helper"),
211 | pytest.param("module_c/submodule2.py", "call_helper_twice", id="submodule2 imports call_helper_twice"),
212 | pytest.param("typing_examples.py", "List", id="typing_examples imports List"),
213 | pytest.param("typing_examples.py", "Dict", marks=pytest.mark.skip(reason="Indexer does not capture imports after ','"), id="typing_examples imports Dict"),
214 | pytest.param("typing_examples.py", "Union", marks=pytest.mark.skip(reason="Indexer does not capture imports after ','"), id="typing_examples imports Union"),
215 | pytest.param("control_flow.py", "numpy", marks=pytest.mark.skip(reason="Indexer does not capture conditional imports"), id="control_flow imports numpy (conditional)"),
216 | pytest.param("control_flow.py", "ujson", marks=pytest.mark.skip(reason="Indexer does not capture conditional imports"), id="control_flow imports ujson (conditional)"),
217 | pytest.param("control_flow.py", "json", id="control_flow imports json (conditional)"),
218 | pytest.param("dynamic_imports.py", "ujson", marks=pytest.mark.skip(reason="Indexer does not capture conditional imports"), id="dynamic_imports imports ujson (conditional)"),
219 | pytest.param("dynamic_imports.py", "json", id="dynamic_imports imports json (conditional)"),
220 | ]
221 |
222 | EXPECTED_PARAMETERS = [
223 | pytest.param("foo", "module_a.py", "x", id="foo has parameter x"),
224 | pytest.param("helper", "module_b.py", "x", id="helper has parameter x"),
225 | pytest.param("process_data", "module_b.py", "data", id="process_data has parameter data"),
226 | pytest.param("factorial", "module_b.py", "n", id="factorial has parameter n"),
227 | pytest.param("square", "advanced_calls.py", "x", id="square has parameter x"),
228 | pytest.param("method", "advanced_calls.py", "x", id="Dummy.method has parameter x"),
229 | pytest.param("with_defaults", "advanced_functions.py", "a", id="with_defaults has parameter a"),
230 | pytest.param("with_defaults", "advanced_functions.py", "b", id="with_defaults has parameter b"),
231 | pytest.param("with_defaults", "advanced_functions.py", "c", id="with_defaults has parameter c"),
232 | pytest.param("higher_order", "advanced_functions.py", "func", id="higher_order has parameter func"),
233 | pytest.param("higher_order", "advanced_functions.py", "data", id="higher_order has parameter data"),
234 | pytest.param("return_function", "advanced_functions.py", "x", id="return_function has parameter x"),
235 | pytest.param("executor", "callbacks_decorators.py", "func", id="executor has parameter func"),
236 | pytest.param("executor", "callbacks_decorators.py", "val", id="executor has parameter val"),
237 | pytest.param("square", "callbacks_decorators.py", "x", id="square has parameter x"),
238 | pytest.param("log_decorator", "callbacks_decorators.py", "fn", id="log_decorator has parameter fn"),
239 | pytest.param("hello", "callbacks_decorators.py", "name", id="hello has parameter name"),
240 | pytest.param("greet", "class_instantiation.py", "self", id="A.greet has parameter self"),
241 | pytest.param("greet", "class_instantiation.py", "self", id="B.greet has parameter self"),
242 | pytest.param("step1", "class_instantiation.py", "self", id="Fluent.step1 has parameter self"),
243 | pytest.param("step2", "class_instantiation.py", "self", id="Fluent.step2 has parameter self"),
244 | pytest.param("step3", "class_instantiation.py", "self", id="Fluent.step3 has parameter self"),
245 | pytest.param("run", "cli_and_dunder.py", "argv", id="run has parameter argv"),
246 | pytest.param("greet", "complex_classes.py", "self", id="Base.greet has self"),
247 | pytest.param("greet", "complex_classes.py", "self", id="Child.greet has self"),
248 | pytest.param("static_method", "complex_classes.py", "x", id="Child.static_method has x"),
249 | pytest.param("class_method", "complex_classes.py", "cls", id="Child.class_method has cls"),
250 | pytest.param("class_method", "complex_classes.py", "y", id="Child.class_method has y"),
251 | pytest.param("decorator", "complex_classes.py", "func", id="decorator has func"),
252 | pytest.param("decorated_function", "complex_classes.py", "x", id="decorated_function has x"),
253 | pytest.param("double", "comprehensions_generators.py", "x", id="double has parameter x"),
254 | pytest.param("__enter__", "context_managers.py", "self", id="FileOpener.__enter__ has self"),
255 | pytest.param("__exit__", "context_managers.py", "self", id="FileOpener.__exit__ has self"),
256 | pytest.param("__exit__", "context_managers.py", "exc_type", id="FileOpener.__exit__ has exc_type"),
257 | pytest.param("__exit__", "context_managers.py", "exc_val", id="FileOpener.__exit__ has exc_val"),
258 | pytest.param("__exit__", "context_managers.py", "exc_tb", id="FileOpener.__exit__ has exc_tb"),
259 | pytest.param("choose_path", "control_flow.py", "x", id="choose_path has parameter x"),
260 | pytest.param("ternary", "control_flow.py", "x", id="ternary has parameter x"),
261 | pytest.param("try_except_finally", "control_flow.py", "x", id="try_except_finally has parameter x"),
262 | pytest.param("conditional_inner_import", "control_flow.py", "use_numpy", id="conditional_inner_import has use_numpy"),
263 | pytest.param("add", "dynamic_dispatch.py", "a", id="add has a"),
264 | pytest.param("add", "dynamic_dispatch.py", "b", id="add has b"),
265 | pytest.param("sub", "dynamic_dispatch.py", "a", id="sub has a"),
266 | pytest.param("sub", "dynamic_dispatch.py", "b", id="sub has b"),
267 | pytest.param("mul", "dynamic_dispatch.py", "a", id="mul has a"),
268 | pytest.param("mul", "dynamic_dispatch.py", "b", id="mul has b"),
269 | pytest.param("dispatch_by_key", "dynamic_dispatch.py", "name", id="dispatch_by_key has name"),
270 | pytest.param("dispatch_by_key", "dynamic_dispatch.py", "a", id="dispatch_by_key has a"),
271 | pytest.param("dispatch_by_key", "dynamic_dispatch.py", "b", id="dispatch_by_key has b"),
272 | pytest.param("method", "dynamic_dispatch.py", "x", id="C.method has x"),
273 | pytest.param("methodcaller_example", "dynamic_dispatch.py", "x", id="methodcaller_example has x"),
274 | pytest.param("import_by___import__", "dynamic_imports.py", "name", id="import_by___import__ has name"),
275 | pytest.param("importlib_runtime", "dynamic_imports.py", "name", id="importlib_runtime has name"),
276 | pytest.param("importlib_runtime", "dynamic_imports.py", "attr", id="importlib_runtime has attr"),
277 | pytest.param("f1", "function_chains.py", "x", id="f1 has x"),
278 | pytest.param("f2", "function_chains.py", "x", id="f2 has x"),
279 | pytest.param("f3", "function_chains.py", "x", id="f3 has x"),
280 | pytest.param("make_adder", "function_chains.py", "n", id="make_adder has n"),
281 | pytest.param("gen_numbers", "generators.py", "n", id="gen_numbers has n"),
282 | pytest.param("agen_numbers", "generators.py", "n", id="agen_numbers has n"),
283 | pytest.param("call", "mapping_calls.py", "cmd", id="Dispatcher.call has cmd"),
284 | pytest.param("use_dispatcher", "mapping_calls.py", "cmd", id="use_dispatcher has cmd"),
285 | pytest.param("call_helper_twice", "module_c/submodule1.py", "x", id="call_helper_twice has x"),
286 | pytest.param("wrapper", "module_c/submodule2.py", "x", id="wrapper has x"),
287 | pytest.param("matcher", "pattern_matching.py", "x", id="matcher has x"),
288 | pytest.param("typed_func", "typing_examples.py", "a", marks=pytest.mark.skip(reason="Indexer does not support parameters with type hints"), id="typed_func has a"),
289 | pytest.param("typed_func", "typing_examples.py", "b", marks=pytest.mark.skip(reason="Indexer does not support parameters with type hints"), id="typed_func has b"),
290 | pytest.param("union_func", "typing_examples.py", "x", marks=pytest.mark.skip(reason="Indexer does not support parameters with type hints"), id="union_func has x"),
291 | pytest.param("dict_func", "typing_examples.py", "d", marks=pytest.mark.skip(reason="Indexer does not support parameters with type hints"), id="dict_func has d"),
292 | pytest.param("wrapper", "complex_classes.py", "*args", marks=pytest.mark.skip(reason="Indexer does not capture variadic parameters (*args)"), id="wrapper has *args"),
293 | pytest.param("wrapper", "complex_classes.py", "**kwargs", marks=pytest.mark.skip(reason="Indexer does not capture variadic parameters (**kwargs)"), id="wrapper has **kwargs"),
294 | pytest.param("dispatch_by_string", "dynamic_dispatch.py", "*args", marks=pytest.mark.skip(reason="Indexer does not capture variadic parameters (*args)"), id="dispatch_by_string has *args"),
295 | pytest.param("dispatch_by_string", "dynamic_dispatch.py", "**kwargs", marks=pytest.mark.skip(reason="Indexer does not capture variadic parameters (**kwargs)"), id="dispatch_by_string has **kwargs"),
296 | pytest.param("dynamic_import_call", "dynamic_dispatch.py", "*args", marks=pytest.mark.skip(reason="Indexer does not capture variadic parameters (*args)"), id="dynamic_import_call has *args"),
297 | pytest.param("dynamic_import_call", "dynamic_dispatch.py", "**kwargs", marks=pytest.mark.skip(reason="Indexer does not capture variadic parameters (**kwargs)"), id="dynamic_import_call has **kwargs"),
298 | pytest.param("adder", "function_chains.py", "x", id="adder has x"),
299 | pytest.param("__aenter__", "generators.py", "self", id="AsyncCM.__aenter__ has self"),
300 | pytest.param("__aexit__", "generators.py", "self", id="AsyncCM.__aexit__ has self"),
301 | pytest.param("__aexit__", "generators.py", "exc_type", id="AsyncCM.__aexit__ has exc_type"),
302 | pytest.param("__aexit__", "generators.py", "exc_val", id="AsyncCM.__aexit__ has exc_val"),
303 | pytest.param("__aexit__", "generators.py", "exc_tb", id="AsyncCM.__aexit__ has exc_tb"),
304 | pytest.param("__init__", "mapping_calls.py", "self", id="Dispatcher.__init__ has self"),
305 | pytest.param("start", "mapping_calls.py", "self", id="Dispatcher.start has self"),
306 | pytest.param("stop", "mapping_calls.py", "self", id="Dispatcher.stop has self"),
307 | pytest.param("ns_func", "namespace_pkg/ns_module.py", None, marks=pytest.mark.skip(reason="Functions with no parameters are not explicitly tested for parameter existence"), id="ns_func has no parameters"),
308 | ]
309 |
310 | EXPECTED_CLASS_METHODS = [
311 | pytest.param("A", "advanced_classes.py", "foo", id="A contains foo"),
312 | pytest.param("B", "advanced_classes.py", "foo", id="B contains foo"),
313 | pytest.param("C", "advanced_classes.py", "bar", id="C contains bar"),
314 | pytest.param("AbstractThing", "advanced_classes.py", "do", id="AbstractThing contains do"),
315 | pytest.param("ConcreteThing", "advanced_classes.py", "do", id="ConcreteThing contains do"),
316 | pytest.param("Dummy", "advanced_calls.py", "method", id="Dummy contains method"),
317 | pytest.param("Mixin1", "advanced_classes2.py", "m1", id="Mixin1 contains m1"),
318 | pytest.param("Mixin2", "advanced_classes2.py", "m2", id="Mixin2 contains m2"),
319 | pytest.param("Combined", "advanced_classes2.py", "both", id="Combined contains both"),
320 | pytest.param("Point", "advanced_classes2.py", "magnitude", id="Point contains magnitude"),
321 | pytest.param("Color", "advanced_classes2.py", "is_primary", id="Color contains is_primary"),
322 | pytest.param("A", "class_instantiation.py", "greet", id="A contains greet"),
323 | pytest.param("B", "class_instantiation.py", "greet", id="B contains greet"),
324 | pytest.param("Fluent", "class_instantiation.py", "step1", id="Fluent contains step1"),
325 | pytest.param("Fluent", "class_instantiation.py", "step2", id="Fluent contains step2"),
326 | pytest.param("Fluent", "class_instantiation.py", "step3", id="Fluent contains step3"),
327 | pytest.param("AsyncCM", "generators.py", "__aenter__", id="AsyncCM contains __aenter__"),
328 | pytest.param("AsyncCM", "generators.py", "__aexit__", id="AsyncCM contains __aexit__"),
329 | pytest.param("Base", "complex_classes.py", "greet", id="Base contains greet"),
330 | pytest.param("Child", "complex_classes.py", "greet", id="Child contains greet"),
331 | pytest.param("Child", "complex_classes.py", "static_method", id="Child contains static_method"),
332 | pytest.param("Child", "complex_classes.py", "class_method", id="Child contains class_method"),
333 | pytest.param("FileOpener", "context_managers.py", "__enter__", id="FileOpener contains __enter__"),
334 | pytest.param("FileOpener", "context_managers.py", "__exit__", id="FileOpener contains __exit__"),
335 | pytest.param("C", "dynamic_dispatch.py", "method", id="C contains method"),
336 | pytest.param("Dispatcher", "mapping_calls.py", "__init__", id="Dispatcher contains __init__"),
337 | pytest.param("Dispatcher", "mapping_calls.py", "start", id="Dispatcher contains start"),
338 | pytest.param("Dispatcher", "mapping_calls.py", "stop", id="Dispatcher contains stop"),
339 | pytest.param("Dispatcher", "mapping_calls.py", "call", id="Dispatcher contains call"),
340 | ]
341 |
342 | EXPECTED_FUNCTION_CONTAINS = [
343 | pytest.param("return_function", "advanced_functions.py", "inner", id="return_function contains inner"),
344 | pytest.param("log_decorator", "callbacks_decorators.py", "wrapper", id="log_decorator contains wrapper"),
345 | pytest.param("make_adder", "function_chains.py", "adder", id="make_adder contains adder"),
346 | pytest.param("decorator", "complex_classes.py", "wrapper", id="decorator contains wrapper"),
347 | ]
348 |
349 | EXPECTED_DECORATORS = [
350 | pytest.param("decorated_function", "complex_classes.py", "decorator", "complex_classes.py", id="decorated_function decorated by decorator"),
351 | pytest.param("hello", "callbacks_decorators.py", "log_decorator", "callbacks_decorators.py", id="hello decorated by log_decorator"),
352 | ]
353 |
354 | EXPECTED_EMPTY_FILES = [
355 | pytest.param("edge_cases/comments_only.py", id="comments_only.py is empty"),
356 | pytest.param("edge_cases/docstring_only.py", id="docstring_only.py is empty"),
357 | pytest.param("edge_cases/empty.py", id="empty.py is empty"),
358 | pytest.param("edge_cases/syntax_error.py", marks=pytest.mark.skip(reason="File with syntax error should be skipped or handled gracefully"), id="syntax_error.py is skipped"),
359 | pytest.param("module_c/__init__.py", id="module_c/__init__.py is empty"),
360 | ]
361 |
362 |
363 |
364 | # ==============================================================================
365 | # == TEST IMPLEMENTATIONS
366 | # ==============================================================================
367 |
368 | def check_query(graph, query, description):
369 | """Helper function to execute a Cypher query and assert that a match is found."""
370 | try:
371 | result = graph.query(query)
372 | except Exception as e:
373 | pytest.fail(f"Query failed for {description} with error: {e}\nQuery was:\n{query}")
374 |
375 | assert result is not None, f"Query for {description} returned None.\nQuery was:\n{query}"
376 | assert len(result) > 0, f"Query for {description} returned no records.\nQuery was:\n{query}"
377 | assert result[0].get('count', 0) > 0, f"No match found for {description}.\nQuery was:\n{query}"
378 |
379 | @pytest.mark.parametrize("file_name, item_name, item_label", EXPECTED_STRUCTURE)
380 | def test_file_contains_item(graph, file_name, item_name, item_label):
381 | """Verifies that a File node correctly CONTAINS a Function or Class node."""
382 | description = f"CONTAINS from [{file_name}] to [{item_name}]"
383 | abs_file_path = os.path.join(SAMPLE_PROJECT_PATH, file_name)
384 | query = f"""
385 | MATCH (f:File {{path: '{abs_file_path}'}})-[:CONTAINS]->(item:{item_label} {{name: '{item_name}'}})
386 | RETURN count(*) AS count
387 | """
388 | check_query(graph, query, description)
389 |
390 | @pytest.mark.parametrize("child_name, child_file, parent_name, parent_file", EXPECTED_INHERITANCE)
391 | def test_inheritance_relationship(graph, child_name, child_file, parent_name, parent_file):
392 | """Verifies that an INHERITS relationship exists between two classes."""
393 | description = f"INHERITS from [{child_name}] to [{parent_name}]"
394 | child_path = os.path.join(SAMPLE_PROJECT_PATH, child_file)
395 | parent_path = os.path.join(SAMPLE_PROJECT_PATH, parent_file)
396 | query = f"""
397 | MATCH (child:Class {{name: '{child_name}', file_path: '{child_path}'}})-[:INHERITS]->(parent:Class {{name: '{parent_name}', file_path: '{parent_path}'}})
398 | RETURN count(*) as count
399 | """
400 | check_query(graph, query, description)
401 |
402 | @pytest.mark.parametrize("caller_name, caller_file, caller_class, callee_name, callee_file, callee_class", EXPECTED_CALLS)
403 | def test_function_call_relationship(graph, caller_name, caller_file, caller_class, callee_name, callee_file, callee_class):
404 | """Verifies that a CALLS relationship exists by checking for nodes first, then the relationship."""
405 | caller_path = os.path.join(SAMPLE_PROJECT_PATH, caller_file)
406 | callee_path = os.path.join(SAMPLE_PROJECT_PATH, callee_file)
407 |
408 | # Build match clauses for caller and callee
409 | if caller_class:
410 | caller_match = f"(caller_class:Class {{name: '{caller_class}', file_path: '{caller_path}'}})-[:CONTAINS]->(caller:Function {{name: '{caller_name}'}})"
411 | else:
412 | caller_match = f"(caller:Function {{name: '{caller_name}', file_path: '{caller_path}'}})"
413 |
414 | if callee_class:
415 | callee_match = f"(callee_class:Class {{name: '{callee_class}', file_path: '{callee_path}'}})-[:CONTAINS]->(callee:Function {{name: '{callee_name}'}})"
416 | else:
417 | callee_match = f"(callee:Function {{name: '{callee_name}', file_path: '{callee_path}'}})"
418 |
419 | # 1. Check that the caller node exists
420 | caller_description = f"existence of caller {caller_class or 'Function'} {{name: '{caller_name}'}} in [{caller_file}]"
421 | caller_query = f"""
422 | MATCH {caller_match}
423 | RETURN count(caller) as count
424 | """
425 | check_query(graph, caller_query, caller_description)
426 |
427 | # 2. Check that the callee node exists
428 | callee_description = f"existence of callee {callee_class or 'Function'} {{name: '{callee_name}'}} in [{callee_file}]"
429 | callee_query = f"""
430 | MATCH {callee_match}
431 | RETURN count(callee) as count
432 | """
433 | check_query(graph, callee_query, callee_description)
434 |
435 | # 3. Check that the CALLS relationship exists between them
436 | relationship_description = f"CALLS from [{caller_name}] to [{callee_name}]"
437 | relationship_query = f"""
438 | MATCH {caller_match}
439 | MATCH {callee_match}
440 | MATCH (caller)-[:CALLS]->(callee)
441 | RETURN count(*) as count
442 | """
443 | check_query(graph, relationship_query, relationship_description)
444 |
445 | @pytest.mark.parametrize("file_name, module_name", EXPECTED_IMPORTS)
446 | def test_import_relationship(graph, file_name, module_name):
447 | """Verifies that an IMPORTS relationship exists between a file and a module."""
448 | description = f"IMPORTS from [{file_name}] to [{module_name}]"
449 | abs_file_path = os.path.join(SAMPLE_PROJECT_PATH, file_name)
450 | query = f"""
451 | MATCH (f:File {{path: '{abs_file_path}'}})-[:IMPORTS]->(m:Module {{name: '{module_name}'}})
452 | RETURN count(*) as count
453 | """
454 | check_query(graph, query, description)
455 |
456 | @pytest.mark.parametrize("function_name, file_name, parameter_name", EXPECTED_PARAMETERS)
457 | def test_parameter_relationship(graph, function_name, file_name, parameter_name):
458 | """Verifies that a HAS_PARAMETER relationship exists between a function and a parameter."""
459 | description = f"HAS_PARAMETER from [{function_name}] to [{parameter_name}]"
460 | abs_file_path = os.path.join(SAMPLE_PROJECT_PATH, file_name)
461 | query = f"""
462 | MATCH (f:Function {{name: '{function_name}', file_path: '{abs_file_path}'}})-[:HAS_PARAMETER]->(p:Parameter {{name: '{parameter_name}'}})
463 | RETURN count(*) as count
464 | """
465 | check_query(graph, query, description)
466 |
467 | @pytest.mark.parametrize("class_name, file_name, method_name", EXPECTED_CLASS_METHODS)
468 | def test_class_method_relationship(graph, class_name, file_name, method_name):
469 | """Verifies that a CONTAINS relationship exists between a class and a method."""
470 | description = f"CONTAINS from [{class_name}] to [{method_name}]"
471 | abs_file_path = os.path.join(SAMPLE_PROJECT_PATH, file_name)
472 | query = f"""
473 | MATCH (c:Class {{name: '{class_name}', file_path: '{abs_file_path}'}})-[:CONTAINS]->(m:Function {{name: '{method_name}'}})
474 | RETURN count(*) as count
475 | """
476 | check_query(graph, query, description)
477 |
478 | @pytest.mark.parametrize("outer_function_name, file_name, inner_function_name", EXPECTED_FUNCTION_CONTAINS)
479 | def test_function_contains_relationship(graph, outer_function_name, file_name, inner_function_name):
480 | """Verifies that a CONTAINS relationship exists between an outer function and an inner function."""
481 | description = f"CONTAINS from [{outer_function_name}] to [{inner_function_name}]"
482 | abs_file_path = os.path.join(SAMPLE_PROJECT_PATH, file_name)
483 | query = f"""
484 | MATCH (outer:Function {{name: '{outer_function_name}', file_path: '{abs_file_path}'}})-[:CONTAINS]->(inner:Function {{name: '{inner_function_name}'}})
485 | RETURN count(*) as count
486 | """
487 | check_query(graph, query, description)
488 |
```
--------------------------------------------------------------------------------
/src/codegraphcontext/server.py:
--------------------------------------------------------------------------------
```python
1 | # src/codegraphcontext/server.py
2 | import urllib.parse
3 | import asyncio
4 | import json
5 | import importlib
6 | import stdlibs
7 | import sys
8 | import traceback
9 | import os
10 | import re
11 | from datetime import datetime
12 | from pathlib import Path
13 | from neo4j.exceptions import CypherSyntaxError
14 | from dataclasses import asdict
15 |
16 | from typing import Any, Dict, Coroutine, Optional
17 |
18 | from .prompts import LLM_SYSTEM_PROMPT
19 | from .core import get_database_manager
20 | from .core.jobs import JobManager, JobStatus
21 | from .core.watcher import CodeWatcher
22 | from .tools.graph_builder import GraphBuilder
23 | from .tools.code_finder import CodeFinder
24 | from .tools.package_resolver import get_local_package_path
25 | from .utils.debug_log import debug_log, info_logger, error_logger, warning_logger, debug_logger
26 |
27 | DEFAULT_EDIT_DISTANCE = 2
28 | DEFAULT_FUZZY_SEARCH = False
29 |
30 | class MCPServer:
31 | """
32 | The main MCP Server class.
33 |
34 | This class orchestrates all the major components of the application, including:
35 | - Database connection management (`DatabaseManager` or `FalkorDBManager`)
36 | - Background job tracking (`JobManager`)
37 | - File system watching for live updates (`CodeWatcher`)
38 | - Tool handlers for graph building, code searching, etc.
39 | - The main JSON-RPC communication loop for interacting with an AI assistant.
40 | """
41 |
42 | def __init__(self, loop=None):
43 | """
44 | Initializes the MCP server and its components.
45 |
46 | Args:
47 | loop: The asyncio event loop to use. If not provided, it gets the current
48 | running loop or creates a new one.
49 | """
50 | try:
51 | # Initialize the database manager (Neo4j or FalkorDB Lite based on env var)
52 | # to fail fast if credentials/configuration are wrong.
53 | self.db_manager = get_database_manager()
54 | self.db_manager.get_driver()
55 | except ValueError as e:
56 | raise ValueError(f"Database configuration error: {e}")
57 |
58 | # Initialize managers for jobs and file watching.
59 | self.job_manager = JobManager()
60 |
61 | # Get the current event loop to pass to thread-sensitive components like the graph builder.
62 | if loop is None:
63 | try:
64 | loop = asyncio.get_running_loop()
65 | except RuntimeError:
66 | loop = asyncio.new_event_loop()
67 | asyncio.set_event_loop(loop)
68 | self.loop = loop
69 |
70 | # Initialize all the tool handlers, passing them the necessary managers and the event loop.
71 | self.graph_builder = GraphBuilder(self.db_manager, self.job_manager, loop)
72 | self.code_finder = CodeFinder(self.db_manager)
73 | self.code_watcher = CodeWatcher(self.graph_builder, self.job_manager)
74 |
75 | # Define the tool manifest that will be exposed to the AI assistant.
76 | self._init_tools()
77 |
78 | def _init_tools(self):
79 | """
80 | Defines the complete tool manifest for the LLM.
81 | This dictionary contains the schema for every tool the AI can call,
82 | including its name, description, and input parameters.
83 | """
84 | self.tools = {
85 | "add_code_to_graph": {
86 | "name": "add_code_to_graph",
87 | "description": "Performs a one-time scan of a local folder to add its code to the graph. Ideal for indexing libraries, dependencies, or projects not being actively modified. Returns a job ID for background processing.",
88 | "inputSchema": {
89 | "type": "object",
90 | "properties": {
91 | "path": {"type": "string", "description": "Path to the directory or file to add."},
92 | "is_dependency": {"type": "boolean", "description": "Whether this code is a dependency.", "default": False}
93 | },
94 | "required": ["path"]
95 | }
96 | },
97 | "check_job_status": {
98 | "name": "check_job_status",
99 | "description": "Check the status and progress of a background job.",
100 | "inputSchema": {
101 | "type": "object",
102 | "properties": { "job_id": {"type": "string", "description": "Job ID from a previous tool call"} },
103 | "required": ["job_id"]
104 | }
105 | },
106 | "list_jobs": {
107 | "name": "list_jobs",
108 | "description": "List all background jobs and their current status.",
109 | "inputSchema": {"type": "object", "properties": {}}
110 | },
111 | "find_code": {
112 | "name": "find_code",
113 | "description": "Find relevant code snippets related to a keyword (e.g., function name, class name, or content).",
114 | "inputSchema": {
115 | "type": "object",
116 | "properties": { "query": {"type": "string", "description": "Keyword or phrase to search for"}, "fuzzy_search": {"type": "boolean", "description": "Whether to use fuzzy search", "default": False}, "edit_distance": {"type": "number", "description": "Edit distance for fuzzy search (between 0-2)", "default": 2}},
117 | "required": ["query"]
118 | }
119 | },
120 |
121 | "analyze_code_relationships": {
122 | "name": "analyze_code_relationships",
123 | "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.",
124 | "inputSchema": {
125 | "type": "object",
126 | "properties": {
127 | "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"]},
128 | "target": {"type": "string", "description": "The function, class, or module to analyze."},
129 | "context": {"type": "string", "description": "Optional: specific file path for precise results."}
130 | },
131 | "required": ["query_type", "target"]
132 | }
133 | },
134 | "watch_directory": {
135 | "name": "watch_directory",
136 | "description": "Performs an initial scan of a directory and then continuously monitors it for changes, automatically keeping the graph up-to-date. Ideal for projects under active development. Returns a job ID for the initial scan.",
137 | "inputSchema": {
138 | "type": "object",
139 | "properties": { "path": {"type": "string", "description": "Path to directory to watch"} },
140 | "required": ["path"]
141 | }
142 | },
143 | "execute_cypher_query": {
144 | "name": "execute_cypher_query",
145 | "description": "Fallback tool to run a direct, read-only Cypher query against the code graph. Use this for complex questions not covered by other tools. The graph contains nodes representing code structures and relationships between them. **Schema Overview:**\n- **Nodes:** `Repository`, `File`, `Module`, `Class`, `Function`.\n- **Properties:** Nodes have properties like `name`, `path`, `cyclomatic_complexity` (on Function nodes), and `code`.\n- **Relationships:** `CONTAINS` (e.g., File-[:CONTAINS]->Function), `CALLS` (Function-[:CALLS]->Function or File-[:CALLS]->Function), `IMPORTS` (File-[:IMPORTS]->Module), `INHERITS` (Class-[:INHERITS]->Class).",
146 | "inputSchema": {
147 | "type": "object",
148 | "properties": { "cypher_query": {"type": "string", "description": "The read-only Cypher query to execute."} },
149 | "required": ["cypher_query"]
150 | }
151 | },
152 | "add_package_to_graph": {
153 | "name": "add_package_to_graph",
154 | "description": "Add a package to the graph by discovering its location. Supports multiple languages. Returns immediately with a job ID.",
155 | "inputSchema": {
156 | "type": "object",
157 | "properties": {
158 | "package_name": {"type": "string", "description": "Name of the package to add (e.g., 'requests', 'express', 'moment', 'lodash')."},
159 | "language": {"type": "string", "description": "The programming language of the package.", "enum": ["python", "javascript", "typescript", "java", "c", "go", "ruby", "php","cpp"]},
160 | "is_dependency": {"type": "boolean", "description": "Mark as a dependency.", "default": True}
161 | },
162 | "required": ["package_name", "language"]
163 | }
164 | },
165 | "find_dead_code": {
166 | "name": "find_dead_code",
167 | "description": "Find potentially unused functions (dead code) across the entire indexed codebase, optionally excluding functions with specific decorators.",
168 | "inputSchema": {
169 | "type": "object",
170 | "properties": {
171 | "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": []}
172 | }
173 | }
174 | },
175 | "calculate_cyclomatic_complexity": {
176 | "name": "calculate_cyclomatic_complexity",
177 | "description": "Calculate the cyclomatic complexity of a specific function to measure its complexity.",
178 | "inputSchema": {
179 | "type": "object",
180 | "properties": {
181 | "function_name": {"type": "string", "description": "The name of the function to analyze."},
182 | "file_path": {"type": "string", "description": "Optional: The full path to the file containing the function for a more specific query."}
183 | },
184 | "required": ["function_name"]
185 | }
186 | },
187 | "find_most_complex_functions": {
188 | "name": "find_most_complex_functions",
189 | "description": "Find the most complex functions in the codebase based on cyclomatic complexity.",
190 | "inputSchema": {
191 | "type": "object",
192 | "properties": {
193 | "limit": {"type": "integer", "description": "The maximum number of complex functions to return.", "default": 10}
194 | }
195 | }
196 | },
197 | "list_indexed_repositories": {
198 | "name": "list_indexed_repositories",
199 | "description": "List all indexed repositories.",
200 | "inputSchema": {
201 | "type": "object",
202 | "properties": {}
203 | }
204 | },
205 | "delete_repository": {
206 | "name": "delete_repository",
207 | "description": "Delete an indexed repository from the graph.",
208 | "inputSchema": {
209 | "type": "object",
210 | "properties": {
211 | "repo_path": {"type": "string", "description": "The path of the repository to delete."}
212 | },
213 | "required": ["repo_path"]
214 | }
215 | },
216 | "visualize_graph_query": {
217 | "name": "visualize_graph_query",
218 | "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.",
219 | "inputSchema": {
220 | "type": "object",
221 | "properties": {
222 | "cypher_query": {"type": "string", "description": "The Cypher query to visualize."}
223 | },
224 | "required": ["cypher_query"]
225 | }
226 | },
227 | "list_watched_paths": {
228 | "name": "list_watched_paths",
229 | "description": "Lists all directories currently being watched for live file changes.",
230 | "inputSchema": {"type": "object", "properties": {}}
231 | },
232 | "unwatch_directory": {
233 | "name": "unwatch_directory",
234 | "description": "Stops watching a directory for live file changes.",
235 | "inputSchema": {
236 | "type": "object",
237 | "properties": {
238 | "path": {"type": "string", "description": "The absolute path of the directory to stop watching."}
239 | },
240 | "required": ["path"]
241 | }
242 | }
243 | }
244 |
245 | def get_database_status(self) -> dict:
246 | """Returns the current connection status of the Neo4j database."""
247 | return {"connected": self.db_manager.is_connected()}
248 |
249 |
250 | def execute_cypher_query_tool(self, **args) -> Dict[str, Any]:
251 | """
252 | Tool implementation for executing a read-only Cypher query.
253 |
254 | Important: Includes a safety check to prevent any database modification
255 | by disallowing keywords like CREATE, MERGE, DELETE, etc.
256 | """
257 | cypher_query = args.get("cypher_query")
258 | if not cypher_query:
259 | return {"error": "Cypher query cannot be empty."}
260 |
261 | # Safety Check: Prevent any write operations to the database.
262 | # This check first removes all string literals and then checks for forbidden keywords.
263 | forbidden_keywords = ['CREATE', 'MERGE', 'DELETE', 'SET', 'REMOVE', 'DROP', 'CALL apoc']
264 |
265 | # Regex to match single or double quoted strings, handling escaped quotes.
266 | string_literal_pattern = r'"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\''
267 |
268 | # Remove all string literals from the query.
269 | query_without_strings = re.sub(string_literal_pattern, '', cypher_query)
270 |
271 | # Now, check for forbidden keywords in the query without strings.
272 | for keyword in forbidden_keywords:
273 | if re.search(r'\b' + keyword + r'\b', query_without_strings, re.IGNORECASE):
274 | return {
275 | "error": "This tool only supports read-only queries. Prohibited keywords like CREATE, MERGE, DELETE, SET, etc., are not allowed."
276 | }
277 |
278 | try:
279 | debug_log(f"Executing Cypher query: {cypher_query}")
280 | with self.db_manager.get_driver().session() as session:
281 | result = session.run(cypher_query)
282 | # Convert results to a list of dictionaries for clean JSON serialization.
283 | records = [record.data() for record in result]
284 |
285 | return {
286 | "success": True,
287 | "query": cypher_query,
288 | "record_count": len(records),
289 | "results": records
290 | }
291 |
292 | except CypherSyntaxError as e:
293 | debug_log(f"Cypher syntax error: {str(e)}")
294 | return {
295 | "error": "Cypher syntax error.",
296 | "details": str(e),
297 | "query": cypher_query
298 | }
299 | except Exception as e:
300 | debug_log(f"Error executing Cypher query: {str(e)}")
301 | return {
302 | "error": "An unexpected error occurred while executing the query.",
303 | "details": str(e)
304 | }
305 |
306 | def find_dead_code_tool(self, **args) -> Dict[str, Any]:
307 | """Tool to find potentially dead code across the entire project."""
308 | exclude_decorated_with = args.get("exclude_decorated_with", [])
309 | try:
310 | debug_log("Finding dead code.")
311 | results = self.code_finder.find_dead_code(exclude_decorated_with=exclude_decorated_with)
312 |
313 | return {
314 | "success": True,
315 | "query_type": "dead_code",
316 | "results": results
317 | }
318 | except Exception as e:
319 | debug_log(f"Error finding dead code: {str(e)}")
320 | return {"error": f"Failed to find dead code: {str(e)}"}
321 |
322 | def calculate_cyclomatic_complexity_tool(self, **args) -> Dict[str, Any]:
323 | """Tool to calculate cyclomatic complexity for a given function."""
324 | function_name = args.get("function_name")
325 | file_path = args.get("file_path")
326 |
327 | try:
328 | debug_log(f"Calculating cyclomatic complexity for function: {function_name}")
329 | results = self.code_finder.get_cyclomatic_complexity(function_name, file_path)
330 |
331 | response = {
332 | "success": True,
333 | "function_name": function_name,
334 | "results": results
335 | }
336 | if file_path:
337 | response["file_path"] = file_path
338 |
339 | return response
340 | except Exception as e:
341 | debug_log(f"Error calculating cyclomatic complexity: {str(e)}")
342 | return {"error": f"Failed to calculate cyclomatic complexity: {str(e)}"}
343 |
344 | def find_most_complex_functions_tool(self, **args) -> Dict[str, Any]:
345 | """Tool to find the most complex functions."""
346 | limit = args.get("limit", 10)
347 | try:
348 | debug_log(f"Finding the top {limit} most complex functions.")
349 | results = self.code_finder.find_most_complex_functions(limit)
350 | return {
351 | "success": True,
352 | "limit": limit,
353 | "results": results
354 | }
355 | except Exception as e:
356 | debug_log(f"Error finding most complex functions: {str(e)}")
357 | return {"error": f"Failed to find most complex functions: {str(e)}"}
358 |
359 | def list_indexed_repositories_tool(self, **args) -> Dict[str, Any]:
360 | """Tool to list indexed repositories."""
361 | try:
362 | debug_log("Listing indexed repositories.")
363 | results = self.code_finder.list_indexed_repositories()
364 | return {
365 | "success": True,
366 | "repositories": results
367 | }
368 | except Exception as e:
369 | debug_log(f"Error listing indexed repositories: {str(e)}")
370 | return {"error": f"Failed to list indexed repositories: {str(e)}"}
371 |
372 | def delete_repository_tool(self, **args) -> Dict[str, Any]:
373 | """Tool to delete a repository from the graph."""
374 | repo_path = args.get("repo_path")
375 | try:
376 | debug_log(f"Deleting repository: {repo_path}")
377 | if self.graph_builder.delete_repository_from_graph(repo_path):
378 | return {
379 | "success": True,
380 | "message": f"Repository '{repo_path}' deleted successfully."
381 | }
382 | else:
383 | return {
384 | "success": False,
385 | "message": f"Repository '{repo_path}' not found in the graph."
386 | }
387 | except Exception as e:
388 | debug_log(f"Error deleting repository: {str(e)}")
389 | return {"error": f"Failed to delete repository: {str(e)}"}
390 |
391 | def visualize_graph_query_tool(self, **args) -> Dict[str, Any]:
392 | """Tool to generate a visualization URL (Neo4j URL or FalkorDB HTML file)."""
393 | cypher_query = args.get("cypher_query")
394 | if not cypher_query:
395 | return {"error": "Cypher query cannot be empty."}
396 |
397 | # Check DB Type: FalkorDBManager vs DatabaseManager
398 | is_falkor = "FalkorDB" in self.db_manager.__class__.__name__
399 |
400 | if is_falkor:
401 | try:
402 | data_nodes = []
403 | data_edges = []
404 | seen_nodes = set()
405 |
406 | with self.db_manager.get_driver().session() as session:
407 | result = session.run(cypher_query)
408 | for record in result:
409 | # Iterate all values in the record to find Nodes and Relationships
410 | # record is a FalkorDBRecord (dict-like), values() works
411 | for val in record.values():
412 | # Process Node
413 | if hasattr(val, 'labels') and hasattr(val, 'id'):
414 | nid = val.id
415 | if nid not in seen_nodes:
416 | seen_nodes.add(nid)
417 | lbl = list(val.labels)[0] if val.labels else "Node"
418 | props = getattr(val, 'properties', {}) or {}
419 | name = props.get('name', str(nid))
420 |
421 | color = "#97c2fc"
422 | if "Repository" in val.labels: color = "#ffb3ba"
423 | elif "File" in val.labels: color = "#baffc9"
424 | elif "Class" in val.labels: color = "#bae1ff"
425 | elif "Function" in val.labels: color = "#ffffba"
426 |
427 | data_nodes.append({
428 | "id": nid, "label": name, "group": lbl,
429 | "title": str(props), "color": color
430 | })
431 |
432 | # Process Relationship
433 | # FalkorDB client relationship objects have src_node/dest_node (ids) and relation (type)
434 | # We check for both standard names just in case
435 | src = getattr(val, 'src_node', None)
436 | if src is None: src = getattr(val, 'start_node', None)
437 |
438 | dst = getattr(val, 'dest_node', None)
439 | if dst is None: dst = getattr(val, 'end_node', None)
440 |
441 | if src is not None and dst is not None:
442 | lbl = getattr(val, 'relation', None) or getattr(val, 'type', 'REL')
443 | data_edges.append({
444 | "from": src,
445 | "to": dst,
446 | "label": lbl,
447 | "arrows": "to"
448 | })
449 |
450 | # Generate HTML
451 | html_content = f"""
452 | <!DOCTYPE html>
453 | <html>
454 | <head>
455 | <title>CodeGraphContext Visualization</title>
456 | <script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
457 | <style type="text/css">
458 | #mynetwork {{ width: 100%; height: 100vh; border: 1px solid lightgray; }}
459 | </style>
460 | </head>
461 | <body>
462 | <div id="mynetwork"></div>
463 | <script type="text/javascript">
464 | var nodes = new vis.DataSet({json.dumps(data_nodes)});
465 | var edges = new vis.DataSet({json.dumps(data_edges)});
466 | var container = document.getElementById('mynetwork');
467 | var data = {{ nodes: nodes, edges: edges }};
468 | var options = {{
469 | nodes: {{ shape: 'dot', size: 16 }},
470 | physics: {{ stabilization: false }},
471 | layout: {{ improvedLayout: false }}
472 | }};
473 | var network = new vis.Network(container, data, options);
474 | </script>
475 | </body>
476 | </html>
477 | """
478 | # filename = f"codegraph_viz_{int(datetime.now().timestamp())}.html"
479 | filename = f"codegraph_viz.html"
480 | out_path = Path(os.getcwd()) / filename
481 | with open(out_path, "w") as f:
482 | f.write(html_content)
483 |
484 | return {
485 | "success": True,
486 | "visualization_url": f"file://{out_path}",
487 | "message": f"Visualization generated at {out_path}. Open this file in your browser."
488 | }
489 |
490 | except Exception as e:
491 | debug_log(f"Error generating FalkorDB visualization: {str(e)}")
492 | return {"error": f"Failed to generate visualization: {str(e)}"}
493 |
494 | else:
495 | # Neo4j fallback
496 | try:
497 | encoded_query = urllib.parse.quote(cypher_query)
498 | visualization_url = f"http://localhost:7474/browser/?cmd=edit&arg={encoded_query}"
499 |
500 | return {
501 | "success": True,
502 | "visualization_url": visualization_url,
503 | "message": "Open the URL in your browser to visualize the graph query. The query will be pre-filled for editing."
504 | }
505 | except Exception as e:
506 | debug_log(f"Error generating visualization URL: {str(e)}")
507 | return {"error": f"Failed to generate visualization URL: {str(e)}"}
508 |
509 | def list_watched_paths_tool(self, **args) -> Dict[str, Any]:
510 | """Tool to list all currently watched directory paths."""
511 | try:
512 | paths = self.code_watcher.list_watched_paths()
513 | return {"success": True, "watched_paths": paths}
514 | except Exception as e:
515 | return {"error": f"Failed to list watched paths: {str(e)}"}
516 |
517 | def unwatch_directory_tool(self, **args) -> Dict[str, Any]:
518 | """Tool to stop watching a directory."""
519 | path = args.get("path")
520 | if not path:
521 | return {"error": "Path is a required argument."}
522 | # The watcher class handles the logic of checking if the path is watched
523 | # and returns an error dictionary if not, so we can just call it.
524 | return self.code_watcher.unwatch_directory(path)
525 |
526 | def watch_directory_tool(self, **args) -> Dict[str, Any]:
527 | """
528 | Tool implementation to start watching a directory for changes.
529 | This tool is now smart: it checks if the path exists and if it has already been indexed.
530 | """
531 | path = args.get("path")
532 | if not path:
533 | return {"error": "Path is a required argument."}
534 |
535 | path_obj = Path(path).resolve()
536 | path_str = str(path_obj)
537 |
538 | # 1. Validate the path before the try...except block
539 | if not path_obj.is_dir():
540 | return {
541 | "success": True,
542 | "status": "path_not_found",
543 | "message": f"Path '{path_str}' does not exist or is not a directory."
544 | }
545 | try:
546 | # Check if already watching
547 | if path_str in self.code_watcher.watched_paths:
548 | return {"success": True, "message": f"Already watching directory: {path_str}"}
549 |
550 | # 2. Check if the repository is already indexed
551 | indexed_repos = self.list_indexed_repositories_tool().get("repositories", [])
552 | is_already_indexed = any(Path(repo["path"]).resolve() == path_obj for repo in indexed_repos)
553 |
554 | # 3. Decide whether to perform an initial scan
555 | if is_already_indexed:
556 | # If already indexed, just start the watcher without a scan
557 | self.code_watcher.watch_directory(path_str, perform_initial_scan=False)
558 | return {
559 | "success": True,
560 | "message": f"Path '{path_str}' is already indexed. Now watching for live changes."
561 | }
562 | else:
563 | # If not indexed, perform the scan AND start the watcher
564 | scan_job_result = self.add_code_to_graph_tool(path=path_str, is_dependency=False)
565 | if "error" in scan_job_result:
566 | return scan_job_result
567 |
568 | self.code_watcher.watch_directory(path_str, perform_initial_scan=True)
569 |
570 | return {
571 | "success": True,
572 | "message": f"Path '{path_str}' was not indexed. Started initial scan and now watching for live changes.",
573 | "job_id": scan_job_result.get("job_id"),
574 | "details": "Use check_job_status to monitor the initial scan."
575 | }
576 |
577 | except Exception as e:
578 | error_logger(f"Failed to start watching directory {path}: {e}")
579 | return {"error": f"Failed to start watching directory: {str(e)}"}
580 |
581 | def add_code_to_graph_tool(self, **args) -> Dict[str, Any]:
582 | """
583 | Tool implementation to index a directory of code.
584 |
585 | This creates a background job and runs the indexing asynchronously
586 | so the AI assistant can continue to be responsive.
587 | """
588 | path = args.get("path")
589 | is_dependency = args.get("is_dependency", False)
590 |
591 | try:
592 | path_obj = Path(path).resolve()
593 |
594 | if not path_obj.exists():
595 | return {
596 | "success": True,
597 | "status": "path_not_found",
598 | "message": f"Path '{path}' does not exist."
599 | }
600 |
601 | # Prevent re-indexing the same repository.
602 | indexed_repos = self.list_indexed_repositories_tool().get("repositories", [])
603 | for repo in indexed_repos:
604 | if Path(repo["path"]).resolve() == path_obj:
605 | return {
606 | "success": False,
607 | "message": f"Repository '{path}' is already indexed."
608 | }
609 |
610 | # Estimate time and create a job for the user to track.
611 | total_files, estimated_time = self.graph_builder.estimate_processing_time(path_obj)
612 | job_id = self.job_manager.create_job(str(path_obj), is_dependency)
613 | self.job_manager.update_job(job_id, total_files=total_files, estimated_duration=estimated_time)
614 |
615 | # Create the coroutine for the background task and schedule it on the main event loop.
616 | coro = self.graph_builder.build_graph_from_path_async(
617 | path_obj, is_dependency, job_id
618 | )
619 | asyncio.run_coroutine_threadsafe(coro, self.loop)
620 |
621 | debug_log(f"Started background job {job_id} for path: {str(path_obj)}, is_dependency: {is_dependency}")
622 |
623 | return {
624 | "success": True, "job_id": job_id,
625 | "message": f"Background processing started for {str(path_obj)}",
626 | "estimated_files": total_files,
627 | "estimated_duration_seconds": round(estimated_time, 2),
628 | "estimated_duration_human": f"{int(estimated_time // 60)}m {int(estimated_time % 60)}s" if estimated_time >= 60 else f"{int(estimated_time)}s",
629 | "instructions": f"Use 'check_job_status' with job_id '{job_id}' to monitor progress"
630 | }
631 |
632 | except Exception as e:
633 | debug_log(f"Error creating background job: {str(e)}")
634 | return {"error": f"Failed to start background processing: {str(e)}"}
635 |
636 | def add_package_to_graph_tool(self, **args) -> Dict[str, Any]:
637 | """Tool to add a package to the graph by auto-discovering its location"""
638 | package_name = args.get("package_name")
639 | language = args.get("language")
640 | is_dependency = args.get("is_dependency", True)
641 |
642 | if not language:
643 | return {"error": "The 'language' parameter is required."}
644 |
645 | try:
646 | # Check if the package is already indexed
647 | indexed_repos = self.list_indexed_repositories_tool().get("repositories", [])
648 | for repo in indexed_repos:
649 | if repo.get("is_dependency") and (repo.get("name") == package_name or repo.get("name") == f"{package_name}.py"):
650 | return {
651 | "success": False,
652 | "message": f"Package '{package_name}' is already indexed."
653 | }
654 |
655 | package_path = get_local_package_path(package_name, language)
656 |
657 | if not package_path:
658 | return {"error": f"Could not find package '{package_name}' for language '{language}'. Make sure it's installed."}
659 |
660 | if not os.path.exists(package_path):
661 | return {"error": f"Package path '{package_path}' does not exist"}
662 |
663 | path_obj = Path(package_path)
664 |
665 | total_files, estimated_time = self.graph_builder.estimate_processing_time(path_obj)
666 |
667 | job_id = self.job_manager.create_job(package_path, is_dependency)
668 |
669 | self.job_manager.update_job(job_id, total_files=total_files, estimated_duration=estimated_time)
670 |
671 | coro = self.graph_builder.build_graph_from_path_async(
672 | path_obj, is_dependency, job_id
673 | )
674 | asyncio.run_coroutine_threadsafe(coro, self.loop)
675 |
676 | debug_log(f"Started background job {job_id} for package: {package_name} at {package_path}, is_dependency: {is_dependency}")
677 |
678 | return {
679 | "success": True, "job_id": job_id, "package_name": package_name,
680 | "discovered_path": package_path,
681 | "message": f"Background processing started for package '{package_name}'",
682 | "estimated_files": total_files,
683 | "estimated_duration_seconds": round(estimated_time, 2),
684 | "estimated_duration_human": f"{int(estimated_time // 60)}m {int(estimated_time % 60)}s" if estimated_time >= 60 else f"{int(estimated_time)}s",
685 | "instructions": f"Use 'check_job_status' with job_id '{job_id}' to monitor progress"
686 | }
687 |
688 | except Exception as e:
689 | debug_log(f"Error creating background job for package {package_name}: {str(e)}")
690 | return {"error": f"Failed to start background processing for package '{package_name}': {str(e)}"}
691 |
692 | def check_job_status_tool(self, **args) -> Dict[str, Any]:
693 | """Tool to check job status"""
694 | job_id = args.get("job_id")
695 | if not job_id:
696 | return {"error": "Job ID is a required argument."}
697 |
698 | try:
699 | job = self.job_manager.get_job(job_id)
700 |
701 | if not job:
702 | return {
703 | "success": True, # Return success to avoid generic error wrapper
704 | "status": "not_found",
705 | "message": f"Job with ID '{job_id}' not found. The ID may be incorrect or the job may have been cleared after a server restart."
706 | }
707 |
708 | job_dict = asdict(job)
709 |
710 | if job.status == JobStatus.RUNNING:
711 | if job.estimated_time_remaining:
712 | remaining = job.estimated_time_remaining
713 | job_dict["estimated_time_remaining_human"] = (
714 | f"{int(remaining // 60)}m {int(remaining % 60)}s"
715 | if remaining >= 60 else f"{int(remaining)}s"
716 | )
717 |
718 | if job.start_time:
719 | elapsed = (datetime.now() - job.start_time).total_seconds()
720 | job_dict["elapsed_time_human"] = (
721 | f"{int(elapsed // 60)}m {int(elapsed % 60)}s"
722 | if elapsed >= 60 else f"{int(elapsed)}s"
723 | )
724 |
725 | elif job.status == JobStatus.COMPLETED and job.start_time and job.end_time:
726 | duration = (job.end_time - job.start_time).total_seconds()
727 | job_dict["actual_duration_human"] = (
728 | f"{int(duration // 60)}m {int(duration % 60)}s"
729 | if duration >= 60 else f"{int(duration)}s"
730 | )
731 |
732 | job_dict["start_time"] = job.start_time.strftime("%Y-%m-%d %H:%M:%S")
733 | if job.end_time:
734 | job_dict["end_time"] = job.end_time.strftime("%Y-%m-%d %H:%M:%S")
735 |
736 | job_dict["status"] = job.status.value
737 |
738 | return {"success": True, "job": job_dict}
739 |
740 | except Exception as e:
741 | debug_log(f"Error checking job status: {str(e)}")
742 | return {"error": f"Failed to check job status: {str(e)}"}
743 |
744 | def list_jobs_tool(self) -> Dict[str, Any]:
745 | """Tool to list all jobs"""
746 | try:
747 | jobs = self.job_manager.list_jobs()
748 |
749 | jobs_data = []
750 | for job in jobs:
751 | job_dict = asdict(job)
752 | job_dict["status"] = job.status.value
753 | job_dict["start_time"] = job.start_time.strftime("%Y-%m-%d %H:%M:%S")
754 | if job.end_time:
755 | job_dict["end_time"] = job.end_time.strftime("%Y-%m-%d %H:%M:%S")
756 | jobs_data.append(job_dict)
757 |
758 | jobs_data.sort(key=lambda x: x["start_time"], reverse=True)
759 |
760 | return {"success": True, "jobs": jobs_data, "total_jobs": len(jobs_data)}
761 |
762 | except Exception as e:
763 | debug_log(f"Error listing jobs: {str(e)}")
764 | return {"error": f"Failed to list jobs: {str(e)}"}
765 |
766 | def analyze_code_relationships_tool(self, **args) -> Dict[str, Any]:
767 | """Tool to analyze code relationships"""
768 | query_type = args.get("query_type")
769 | target = args.get("target")
770 | context = args.get("context")
771 |
772 | if not query_type or not target:
773 | return {
774 | "error": "Both 'query_type' and 'target' are required",
775 | "supported_query_types": [
776 | "find_callers", "find_callees", "find_importers", "who_modifies",
777 | "class_hierarchy", "overrides", "dead_code", "call_chain",
778 | "module_deps", "variable_scope", "find_complexity"
779 | ]
780 | }
781 |
782 | try:
783 | debug_log(f"Analyzing relationships: {query_type} for {target}")
784 | results = self.code_finder.analyze_code_relationships(query_type, target, context)
785 |
786 | return {
787 | "success": True, "query_type": query_type, "target": target,
788 | "context": context, "results": results
789 | }
790 |
791 | except Exception as e:
792 | debug_log(f"Error analyzing relationships: {str(e)}")
793 | return {"error": f"Failed to analyze relationships: {str(e)}"}
794 |
795 | @staticmethod
796 | def _normalize(text: str) -> str:
797 | return text.lower().replace("_", " ").strip()
798 |
799 | def find_code_tool(self, **args) -> Dict[str, Any]:
800 | """Tool to find relevant code snippets"""
801 | query = args.get("query")
802 | fuzzy_search = args.get("fuzzy_search", DEFAULT_FUZZY_SEARCH)
803 | edit_distance = args.get("edit_distance", DEFAULT_EDIT_DISTANCE)
804 |
805 | if fuzzy_search:
806 | query = self._normalize(query)
807 |
808 | try:
809 | debug_log(f"Finding code for query: {query} with fuzzy_search={fuzzy_search}, edit_distance={edit_distance}")
810 | results = self.code_finder.find_related_code(query, fuzzy_search, edit_distance)
811 |
812 | return {"success": True, "query": query, "results": results}
813 |
814 | except Exception as e:
815 | debug_log(f"Error finding code: {str(e)}")
816 | return {"error": f"Failed to find code: {str(e)}"}
817 |
818 |
819 | async def handle_tool_call(self, tool_name: str, args: Dict[str, Any]) -> Dict[str, Any]:
820 | """
821 | Routes a tool call from the AI assistant to the appropriate handler function.
822 |
823 | Args:
824 | tool_name: The name of the tool to execute.
825 | args: A dictionary of arguments for the tool.
826 |
827 | Returns:
828 | A dictionary containing the result of the tool execution.
829 | """
830 | tool_map: Dict[str, Coroutine] = {
831 | "add_package_to_graph": self.add_package_to_graph_tool,
832 | "find_dead_code": self.find_dead_code_tool,
833 | "find_code": self.find_code_tool,
834 | "analyze_code_relationships": self.analyze_code_relationships_tool,
835 | "watch_directory": self.watch_directory_tool,
836 | "execute_cypher_query": self.execute_cypher_query_tool,
837 | "add_code_to_graph": self.add_code_to_graph_tool,
838 | "check_job_status": self.check_job_status_tool,
839 | "list_jobs": self.list_jobs_tool,
840 | "calculate_cyclomatic_complexity": self.calculate_cyclomatic_complexity_tool,
841 | "find_most_complex_functions": self.find_most_complex_functions_tool,
842 | "list_indexed_repositories": self.list_indexed_repositories_tool,
843 | "delete_repository": self.delete_repository_tool,
844 | "visualize_graph_query": self.visualize_graph_query_tool,
845 | "list_watched_paths": self.list_watched_paths_tool,
846 | "unwatch_directory": self.unwatch_directory_tool
847 | }
848 | handler = tool_map.get(tool_name)
849 | if handler:
850 | # Run the synchronous tool function in a separate thread to avoid
851 | # blocking the main asyncio event loop.
852 | return await asyncio.to_thread(handler, **args)
853 | else:
854 | return {"error": f"Unknown tool: {tool_name}"}
855 |
856 | async def run(self):
857 | """
858 | Runs the main server loop, listening for JSON-RPC requests from stdin.
859 |
860 | This loop continuously reads lines from stdin, parses them as JSON-RPC
861 | requests, and routes them to the appropriate handlers (e.g., initialize,
862 | tools/list, tools/call). The response is then printed to stdout.
863 | """
864 | # info_logger("MCP Server is running. Waiting for requests...")
865 | print("MCP Server is running. Waiting for requests...", file=sys.stderr, flush=True)
866 | self.code_watcher.start()
867 |
868 | loop = asyncio.get_event_loop()
869 | while True:
870 | try:
871 | # Read a request from the standard input.
872 | line = await loop.run_in_executor(None, sys.stdin.readline)
873 | if not line:
874 | debug_logger("Client disconnected (EOF received). Shutting down.")
875 | break
876 |
877 | request = json.loads(line.strip())
878 | method = request.get('method')
879 | params = request.get('params', {})
880 | request_id = request.get('id')
881 |
882 | response = {}
883 | # Route the request based on the JSON-RPC method.
884 | if method == 'initialize':
885 | response = {
886 | "jsonrpc": "2.0", "id": request_id,
887 | "result": {
888 | "protocolVersion": "2025-03-26",
889 | "serverInfo": {
890 | "name": "CodeGraphContext", "version": "0.1.0",
891 | "systemPrompt": LLM_SYSTEM_PROMPT
892 | },
893 | "capabilities": {"tools": {"listTools": True}},
894 | }
895 | }
896 | elif method == 'tools/list':
897 | # Return the list of tools defined in _init_tools.
898 | response = {
899 | "jsonrpc": "2.0", "id": request_id,
900 | "result": {"tools": list(self.tools.values())}
901 | }
902 | elif method == 'tools/call':
903 | # Execute a tool call and return the result.
904 | tool_name = params.get('name')
905 | args = params.get('arguments', {})
906 | result = await self.handle_tool_call(tool_name, args)
907 |
908 | if "error" in result:
909 | response = {
910 | "jsonrpc": "2.0", "id": request_id,
911 | "error": {"code": -32000, "message": "Tool execution error", "data": result}
912 | }
913 | else:
914 | response = {
915 | "jsonrpc": "2.0", "id": request_id,
916 | "result": {"content": [{"type": "text", "text": json.dumps(result, indent=2)}]}
917 | }
918 | elif method == 'notifications/initialized':
919 | # This is a notification, no response needed.
920 | pass
921 | else:
922 | # Handle unknown methods.
923 | if request_id is not None:
924 | response = {
925 | "jsonrpc": "2.0", "id": request_id,
926 | "error": {"code": -32601, "message": f"Method not found: {method}"}
927 | }
928 |
929 | # Send the response to standard output if it's not a notification.
930 | if request_id is not None and response:
931 | print(json.dumps(response), flush=True)
932 |
933 | except Exception as e:
934 | error_logger(f"Error processing request: {e}\n{traceback.format_exc()}")
935 | request_id = "unknown"
936 | if 'request' in locals() and isinstance(request, dict):
937 | request_id = request.get('id', "unknown")
938 |
939 | error_response = {
940 | "jsonrpc": "2.0", "id": request_id,
941 | "error": {"code": -32603, "message": f"Internal error: {str(e)}", "data": traceback.format_exc()}
942 | }
943 | print(json.dumps(error_response), flush=True)
944 |
945 | def shutdown(self):
946 | """Gracefully shuts down the server and its components."""
947 | debug_logger("Shutting down server...")
948 | self.code_watcher.stop()
949 | self.db_manager.close_driver()
950 |
```
--------------------------------------------------------------------------------
/src/codegraphcontext/tools/code_finder.py:
--------------------------------------------------------------------------------
```python
1 | # src/codegraphcontext/tools/code_finder.py
2 | import logging
3 | import re
4 | from typing import Any, Dict, List, Literal, Optional
5 | from pathlib import Path
6 |
7 | from ..core.database import DatabaseManager
8 |
9 | logger = logging.getLogger(__name__)
10 |
11 | class CodeFinder:
12 | """Module for finding relevant code snippets and analyzing relationships."""
13 |
14 | def __init__(self, db_manager: DatabaseManager):
15 | self.db_manager = db_manager
16 | self.driver = self.db_manager.get_driver()
17 |
18 | def format_query(self, find_by: Literal["Class", "Function"], fuzzy_search:bool) -> str:
19 | """Format the search query based on the search type and fuzzy search settings."""
20 | return f"""
21 | CALL db.index.fulltext.queryNodes("code_search_index", $search_term) YIELD node, score
22 | WITH node, score
23 | WHERE node:{find_by} {'AND node.name CONTAINS $search_term' if not fuzzy_search else ''}
24 | RETURN node.name as name, node.file_path as file_path, node.line_number as line_number,
25 | node.source as source, node.docstring as docstring, node.is_dependency as is_dependency
26 | ORDER BY score DESC
27 | LIMIT 20
28 | """
29 |
30 | def find_by_function_name(self, search_term: str, fuzzy_search: bool) -> List[Dict]:
31 | """Find functions by name matching."""
32 | with self.driver.session() as session:
33 | if not fuzzy_search:
34 | # Use simple match for exact search to avoid fulltext index dependency
35 | result = session.run("""
36 | MATCH (node:Function {name: $name})
37 | RETURN node.name as name, node.file_path as file_path, node.line_number as line_number,
38 | node.source as source, node.docstring as docstring, node.is_dependency as is_dependency
39 | LIMIT 20
40 | """, name=search_term)
41 | return result.data()
42 |
43 | # Fuzzy search using fulltext index
44 | formatted_search_term = f"name:{search_term}"
45 | result = session.run(self.format_query("Function", fuzzy_search), search_term=formatted_search_term)
46 | return result.data()
47 |
48 | def find_by_class_name(self, search_term: str, fuzzy_search: bool) -> List[Dict]:
49 | """Find classes by name matching."""
50 | with self.driver.session() as session:
51 | if not fuzzy_search:
52 | # Use simple match for exact search to avoid fulltext index dependency
53 | result = session.run("""
54 | MATCH (node:Class {name: $name})
55 | RETURN node.name as name, node.file_path as file_path, node.line_number as line_number,
56 | node.source as source, node.docstring as docstring, node.is_dependency as is_dependency
57 | LIMIT 20
58 | """, name=search_term)
59 | return result.data()
60 |
61 | # Fuzzy search using fulltext index
62 | formatted_search_term = f"name:{search_term}"
63 | result = session.run(self.format_query("Class", fuzzy_search), search_term=formatted_search_term)
64 | return result.data()
65 |
66 | def find_by_variable_name(self, search_term: str) -> List[Dict]:
67 | """Find variables by name matching"""
68 | with self.driver.session() as session:
69 | result = session.run("""
70 | MATCH (v:Variable)
71 | WHERE v.name CONTAINS $search_term
72 | RETURN v.name as name, v.file_path as file_path, v.line_number as line_number,
73 | v.value as value, v.context as context, v.is_dependency as is_dependency
74 | ORDER BY v.is_dependency ASC, v.name
75 | LIMIT 20
76 | """, search_term=search_term)
77 |
78 | return result.data()
79 |
80 | def find_by_content(self, search_term: str) -> List[Dict]:
81 | """Find code by content matching in source or docstrings using the full-text index."""
82 | with self.driver.session() as session:
83 | result = session.run("""
84 | CALL db.index.fulltext.queryNodes("code_search_index", $search_term) YIELD node, score
85 | WITH node, score
86 | WHERE node:Function OR node:Class OR node:Variable
87 | RETURN
88 | CASE
89 | WHEN node:Function THEN 'function'
90 | WHEN node:Class THEN 'class'
91 | ELSE 'variable'
92 | END as type,
93 | node.name as name, node.file_path as file_path,
94 | node.line_number as line_number, node.source as source,
95 | node.docstring as docstring, node.is_dependency as is_dependency
96 | ORDER BY score DESC
97 | LIMIT 20
98 | """, search_term=search_term)
99 | return result.data()
100 |
101 | def find_by_module_name(self, search_term: str) -> List[Dict]:
102 | """Find modules by name matching"""
103 | with self.driver.session() as session:
104 | result = session.run("""
105 | MATCH (m:Module)
106 | WHERE m.name CONTAINS $search_term
107 | RETURN m.name as name, m.lang as lang
108 | ORDER BY m.name
109 | LIMIT 20
110 | """, search_term=search_term)
111 | return result.data()
112 |
113 | def find_imports(self, search_term: str) -> List[Dict]:
114 | """Find imported symbols (aliases or original names)."""
115 | with self.driver.session() as session:
116 | result = session.run("""
117 | MATCH (f:File)-[r:IMPORTS]->(m:Module)
118 | WHERE r.alias = $search_term OR r.imported_name = $search_term
119 | RETURN
120 | r.alias as alias,
121 | r.imported_name as imported_name,
122 | m.name as module_name,
123 | f.path as file_path,
124 | r.line_number as line_number
125 | ORDER BY f.path
126 | LIMIT 20
127 | """, search_term=search_term)
128 | return result.data()
129 |
130 | def find_related_code(self, user_query: str, fuzzy_search: bool, edit_distance: int) -> Dict[str, Any]:
131 | """Find code related to a query using multiple search strategies"""
132 | if fuzzy_search:
133 | user_query_normalized = " ".join(map(lambda x: f"{x}~{edit_distance}", user_query.split(" ")))
134 | else:
135 | user_query_normalized = user_query
136 |
137 | results = {
138 | "query": user_query_normalized,
139 | "functions_by_name": self.find_by_function_name(user_query_normalized, fuzzy_search),
140 | "classes_by_name": self.find_by_class_name(user_query_normalized, fuzzy_search),
141 | "variables_by_name": self.find_by_variable_name(user_query), # no fuzzy for variables as they are not using full-text index
142 | "content_matches": self.find_by_content(user_query_normalized)
143 | }
144 |
145 | all_results = []
146 |
147 | for func in results["functions_by_name"]:
148 | func["search_type"] = "function_name"
149 | func["relevance_score"] = 0.9 if not func["is_dependency"] else 0.7
150 | all_results.append(func)
151 |
152 | for cls in results["classes_by_name"]:
153 | cls["search_type"] = "class_name"
154 | cls["relevance_score"] = 0.8 if not cls["is_dependency"] else 0.6
155 | all_results.append(cls)
156 |
157 | for var in results["variables_by_name"]:
158 | var["search_type"] = "variable_name"
159 | var["relevance_score"] = 0.7 if not var["is_dependency"] else 0.5
160 | all_results.append(var)
161 |
162 | for content in results["content_matches"]:
163 | content["search_type"] = "content"
164 | content["relevance_score"] = 0.6 if not content["is_dependency"] else 0.4
165 | all_results.append(content)
166 |
167 | all_results.sort(key=lambda x: x["relevance_score"], reverse=True)
168 |
169 | results["ranked_results"] = all_results[:15]
170 | results["total_matches"] = len(all_results)
171 |
172 | return results
173 |
174 | def find_functions_by_argument(self, argument_name: str, file_path: str = None) -> List[Dict]:
175 | """Find functions that take a specific argument name."""
176 | with self.driver.session() as session:
177 | if file_path:
178 | query = """
179 | MATCH (f:Function)-[:HAS_PARAMETER]->(p:Parameter)
180 | WHERE p.name = $argument_name AND f.file_path = $file_path
181 | RETURN f.name AS function_name, f.file_path AS file_path, f.line_number AS line_number,
182 | f.docstring AS docstring, f.is_dependency AS is_dependency
183 | ORDER BY f.is_dependency ASC, f.file_path, f.line_number
184 | LIMIT 20
185 | """
186 | result = session.run(query, argument_name=argument_name, file_path=file_path)
187 | else:
188 | query = """
189 | MATCH (f:Function)-[:HAS_PARAMETER]->(p:Parameter)
190 | WHERE p.name = $argument_name
191 | RETURN f.name AS function_name, f.file_path AS file_path, f.line_number AS line_number,
192 | f.docstring AS docstring, f.is_dependency AS is_dependency
193 | ORDER BY f.is_dependency ASC, f.file_path, f.line_number
194 | LIMIT 20
195 | """
196 | result = session.run(query, argument_name=argument_name)
197 | return result.data()
198 |
199 | def find_functions_by_decorator(self, decorator_name: str, file_path: str = None) -> List[Dict]:
200 | """Find functions that have a specific decorator applied to them."""
201 | with self.driver.session() as session:
202 | if file_path:
203 | query = """
204 | MATCH (f:Function)
205 | WHERE f.file_path = $file_path AND $decorator_name IN f.decorators
206 | RETURN f.name AS function_name, f.file_path AS file_path, f.line_number AS line_number,
207 | f.docstring AS docstring, f.is_dependency AS is_dependency, f.decorators AS decorators
208 | ORDER BY f.is_dependency ASC, f.file_path, f.line_number
209 | LIMIT 20
210 | """
211 | result = session.run(query, decorator_name=decorator_name, file_path=file_path)
212 | else:
213 | query = """
214 | MATCH (f:Function)
215 | WHERE $decorator_name IN f.decorators
216 | RETURN f.name AS function_name, f.file_path AS file_path, f.line_number AS line_number,
217 | f.docstring AS docstring, f.is_dependency AS is_dependency, f.decorators AS decorators
218 | ORDER BY f.is_dependency ASC, f.file_path, f.line_number
219 | LIMIT 20
220 | """
221 | result = session.run(query, decorator_name=decorator_name)
222 | return result.data()
223 |
224 | def who_calls_function(self, function_name: str, file_path: str = None) -> List[Dict]:
225 | """Find what functions call a specific function using CALLS relationships with improved matching"""
226 | with self.driver.session() as session:
227 | if file_path:
228 | result = session.run("""
229 | MATCH (caller:Function)-[call:CALLS]->(target:Function {name: $function_name, file_path: $file_path})
230 | OPTIONAL MATCH (caller_file:File)-[:CONTAINS]->(caller)
231 | RETURN DISTINCT
232 | caller.name as caller_function,
233 | caller.file_path as caller_file_path,
234 | caller.line_number as caller_line_number,
235 | caller.docstring as caller_docstring,
236 | caller.is_dependency as caller_is_dependency,
237 | call.line_number as call_line_number,
238 | call.args as call_args,
239 | call.full_call_name as full_call_name,
240 | target.file_path as target_file_path
241 | ORDER BY caller.is_dependency ASC, caller.file_path, caller.line_number
242 | LIMIT 20
243 | """, function_name=function_name, file_path=file_path)
244 |
245 | results = [dict(record) for record in result]
246 | if not results:
247 | result = session.run("""
248 | MATCH (target:Function {name: $function_name})
249 | MATCH (caller:Function)-[call:CALLS]->(target)
250 | OPTIONAL MATCH (caller_file:File)-[:CONTAINS]->(caller)
251 | RETURN DISTINCT
252 | caller.name as caller_function,
253 | caller.file_path as caller_file_path,
254 | caller.line_number as caller_line_number,
255 | caller.docstring as caller_docstring,
256 | caller.is_dependency as caller_is_dependency,
257 | call.line_number as call_line_number,
258 | call.args as call_args,
259 | call.full_call_name as full_call_name,
260 | target.file_path as target_file_path
261 | ORDER BY caller.is_dependency ASC, caller.file_path, caller.line_number
262 | LIMIT 20
263 | """, function_name=function_name)
264 | results = [dict(record) for record in result]
265 | else:
266 | result = session.run("""
267 | MATCH (target:Function {name: $function_name})
268 | MATCH (caller:Function)-[call:CALLS]->(target)
269 | OPTIONAL MATCH (caller_file:File)-[:CONTAINS]->(caller)
270 | RETURN DISTINCT
271 | caller.name as caller_function,
272 | caller.file_path as caller_file_path,
273 | caller.line_number as caller_line_number,
274 | caller.docstring as caller_docstring,
275 | caller.is_dependency as caller_is_dependency,
276 | call.line_number as call_line_number,
277 | call.args as call_args,
278 | call.full_call_name as full_call_name,
279 | target.file_path as target_file_path
280 | ORDER BY caller.is_dependency ASC, caller.file_path, caller.line_number
281 | LIMIT 20
282 | """, function_name=function_name)
283 | results = [dict(record) for record in result]
284 |
285 | return results
286 |
287 | def what_does_function_call(self, function_name: str, file_path: str = None) -> List[Dict]:
288 | """Find what functions a specific function calls using CALLS relationships"""
289 | with self.driver.session() as session:
290 | if file_path:
291 | # Convert file_path to absolute path
292 | absolute_file_path = str(Path(file_path).resolve())
293 | result = session.run("""
294 | MATCH (caller:Function {name: $function_name, file_path: $absolute_file_path})
295 | MATCH (caller)-[call:CALLS]->(called:Function)
296 | OPTIONAL MATCH (called_file:File)-[:CONTAINS]->(called)
297 | RETURN DISTINCT
298 | called.name as called_function,
299 | called.file_path as called_file_path,
300 | called.line_number as called_line_number,
301 | called.docstring as called_docstring,
302 | called.is_dependency as called_is_dependency,
303 | call.line_number as call_line_number,
304 | call.args as call_args,
305 | call.full_call_name as full_call_name
306 | ORDER BY called.is_dependency ASC, called.name
307 | LIMIT 20
308 | """, function_name=function_name, absolute_file_path=absolute_file_path)
309 | else:
310 | result = session.run("""
311 | MATCH (caller:Function {name: $function_name})
312 | MATCH (caller)-[call:CALLS]->(called:Function)
313 | OPTIONAL MATCH (called_file:File)-[:CONTAINS]->(called)
314 | RETURN DISTINCT
315 | called.name as called_function,
316 | called.file_path as called_file_path,
317 | called.line_number as called_line_number,
318 | called.docstring as called_docstring,
319 | called.is_dependency as called_is_dependency,
320 | call.line_number as call_line_number,
321 | call.args as call_args,
322 | call.full_call_name as full_call_name
323 | ORDER BY called.is_dependency ASC, called.name
324 | LIMIT 20
325 | """, function_name=function_name)
326 |
327 | return result.data()
328 |
329 | def who_imports_module(self, module_name: str) -> List[Dict]:
330 | """Find what files import a specific module using IMPORTS relationships"""
331 | with self.driver.session() as session:
332 | result = session.run("""
333 | MATCH (file:File)-[imp:IMPORTS]->(module:Module)
334 | WHERE module.name = $module_name OR module.full_import_name CONTAINS $module_name
335 | OPTIONAL MATCH (repo:Repository)-[:CONTAINS]->(file)
336 | WITH file, repo, COLLECT({
337 | imported_module: module.name,
338 | import_alias: module.alias,
339 | full_import_name: module.full_import_name
340 | }) AS imports
341 | RETURN
342 | file.name AS file_name,
343 | file.path AS file_path,
344 | file.relative_path AS file_relative_path,
345 | file.is_dependency AS file_is_dependency,
346 | repo.name AS repository_name,
347 | imports
348 | ORDER BY file.is_dependency ASC, file.path
349 | LIMIT 20
350 | """, module_name=module_name)
351 |
352 | return result.data()
353 |
354 | def who_modifies_variable(self, variable_name: str) -> List[Dict]:
355 | """Find what functions contain or modify a specific variable"""
356 | with self.driver.session() as session:
357 | result = session.run("""
358 | MATCH (var:Variable {name: $variable_name})
359 | MATCH (container)-[:CONTAINS]->(var)
360 | WHERE container:Function OR container:Class OR container:File
361 | OPTIONAL MATCH (file:File)-[:CONTAINS]->(container)
362 | RETURN DISTINCT
363 | CASE
364 | WHEN container:Function THEN container.name
365 | WHEN container:Class THEN container.name
366 | ELSE 'file_level'
367 | END as container_name,
368 | CASE
369 | WHEN container:Function THEN 'function'
370 | WHEN container:Class THEN 'class'
371 | ELSE 'file'
372 | END as container_type,
373 | COALESCE(container.file_path, file.path) as file_path,
374 | container.line_number as container_line_number,
375 | var.line_number as variable_line_number,
376 | var.value as variable_value,
377 | var.context as variable_context,
378 | COALESCE(container.is_dependency, file.is_dependency, false) as is_dependency
379 | ORDER BY is_dependency ASC, file_path, variable_line_number
380 | LIMIT 20
381 | """, variable_name=variable_name)
382 |
383 | return result.data()
384 |
385 | def find_class_hierarchy(self, class_name: str, file_path: str = None) -> Dict[str, Any]:
386 | """Find class inheritance relationships using INHERITS relationships"""
387 | with self.driver.session() as session:
388 | if file_path:
389 | match_clause = "MATCH (child:Class {name: $class_name, file_path: $file_path})"
390 | else:
391 | match_clause = "MATCH (child:Class {name: $class_name})"
392 |
393 | parents_query = f"""
394 | {match_clause}
395 | MATCH (child)-[:INHERITS]->(parent:Class)
396 | OPTIONAL MATCH (parent_file:File)-[:CONTAINS]->(parent)
397 | RETURN DISTINCT
398 | parent.name as parent_class,
399 | parent.file_path as parent_file_path,
400 | parent.line_number as parent_line_number,
401 | parent.docstring as parent_docstring,
402 | parent.is_dependency as parent_is_dependency
403 | ORDER BY parent.is_dependency ASC, parent.name
404 | """
405 | parents_result = session.run(parents_query, class_name=class_name, file_path=file_path)
406 |
407 | children_query = f"""
408 | {match_clause}
409 | MATCH (grandchild:Class)-[:INHERITS]->(child)
410 | OPTIONAL MATCH (child_file:File)-[:CONTAINS]->(grandchild)
411 | RETURN DISTINCT
412 | grandchild.name as child_class,
413 | grandchild.file_path as child_file_path,
414 | grandchild.line_number as child_line_number,
415 | grandchild.docstring as child_docstring,
416 | grandchild.is_dependency as child_is_dependency
417 | ORDER BY grandchild.is_dependency ASC, grandchild.name
418 | """
419 | children_result = session.run(children_query, class_name=class_name, file_path=file_path)
420 |
421 | methods_query = f"""
422 | {match_clause}
423 | MATCH (child)-[:CONTAINS]->(method:Function)
424 | RETURN DISTINCT
425 | method.name as method_name,
426 | method.file_path as method_file_path,
427 | method.line_number as method_line_number,
428 | method.args as method_args,
429 | method.docstring as method_docstring,
430 | method.is_dependency as method_is_dependency
431 | ORDER BY method.is_dependency ASC, method.line_number
432 | """
433 | methods_result = session.run(methods_query, class_name=class_name, file_path=file_path)
434 |
435 | return {
436 | "class_name": class_name,
437 | "parent_classes": [dict(record) for record in parents_result],
438 | "child_classes": [dict(record) for record in children_result],
439 | "methods": [dict(record) for record in methods_result]
440 | }
441 |
442 | def find_function_overrides(self, function_name: str) -> List[Dict]:
443 | """Find all implementations of a function across different classes"""
444 | with self.driver.session() as session:
445 | result = session.run("""
446 | MATCH (class:Class)-[:CONTAINS]->(func:Function {name: $function_name})
447 | OPTIONAL MATCH (file:File)-[:CONTAINS]->(class)
448 | RETURN DISTINCT
449 | class.name as class_name,
450 | class.file_path as class_file_path,
451 | func.name as function_name,
452 | func.line_number as function_line_number,
453 | func.args as function_args,
454 | func.docstring as function_docstring,
455 | func.is_dependency as is_dependency,
456 | file.name as file_name
457 | ORDER BY func.is_dependency ASC, class.name
458 | LIMIT 20
459 | """, function_name=function_name)
460 |
461 | return result.data()
462 |
463 | def find_dead_code(self, exclude_decorated_with: List[str] = None) -> Dict[str, Any]:
464 | """Find potentially unused functions (not called by other functions in the project), optionally excluding those with specific decorators."""
465 | if exclude_decorated_with is None:
466 | exclude_decorated_with = []
467 |
468 | with self.driver.session() as session:
469 | result = session.run("""
470 | MATCH (func:Function)
471 | WHERE func.is_dependency = false
472 | AND NOT func.name IN ['main', '__init__', '__main__', 'setup', 'run', '__new__', '__del__']
473 | AND NOT func.name STARTS WITH '_test'
474 | AND NOT func.name STARTS WITH 'test_'
475 | AND ALL(decorator_name IN $exclude_decorated_with WHERE NOT decorator_name IN func.decorators)
476 | WITH func
477 | OPTIONAL MATCH (caller:Function)-[:CALLS]->(func)
478 | WHERE caller.is_dependency = false
479 | WITH func, count(caller) as caller_count
480 | WHERE caller_count = 0
481 | OPTIONAL MATCH (file:File)-[:CONTAINS]->(func)
482 | RETURN
483 | func.name as function_name,
484 | func.file_path as file_path,
485 | func.line_number as line_number,
486 | func.docstring as docstring,
487 | func.context as context,
488 | file.name as file_name
489 | ORDER BY func.file_path, func.line_number
490 | LIMIT 50
491 | """, exclude_decorated_with=exclude_decorated_with)
492 |
493 | return {
494 | "potentially_unused_functions": [dict(record) for record in result],
495 | "note": "These functions might be unused, but could be entry points, callbacks, or called dynamically"
496 | }
497 |
498 | def find_all_callers(self, function_name: str, file_path: str = None) -> List[Dict]:
499 | """Find all direct and indirect callers of a specific function."""
500 | with self.driver.session() as session:
501 | if file_path:
502 | # Find functions within the specified file_path that call the target function
503 | query = """
504 | MATCH (f:Function)-[:CALLS*]->(target:Function {name: $function_name, file_path: $file_path})
505 | 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
506 | ORDER BY f.is_dependency ASC, f.file_path, f.line_number
507 | LIMIT 50
508 | """
509 | result = session.run(query, function_name=function_name, file_path=file_path)
510 | else:
511 | # If no file_path (context) is provided, find all callers of the function by name
512 | query = """
513 | MATCH (f:Function)-[:CALLS*]->(target:Function {name: $function_name})
514 | 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
515 | ORDER BY f.is_dependency ASC, f.file_path, f.line_number
516 | LIMIT 50
517 | """
518 | result = session.run(query, function_name=function_name)
519 | return result.data()
520 |
521 | def find_all_callees(self, function_name: str, file_path: str = None) -> List[Dict]:
522 | """Find all direct and indirect callees of a specific function."""
523 | with self.driver.session() as session:
524 | if file_path:
525 | query = """
526 | MATCH (caller:Function {name: $function_name, file_path: $file_path})
527 | MATCH (caller)-[:CALLS*]->(f:Function)
528 | 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
529 | ORDER BY f.is_dependency ASC, f.file_path, f.line_number
530 | LIMIT 50
531 | """
532 | result = session.run(query, function_name=function_name, file_path=file_path)
533 | else:
534 | query = """
535 | MATCH (caller:Function {name: $function_name})
536 | MATCH (caller)-[:CALLS*]->(f:Function)
537 | 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
538 | ORDER BY f.is_dependency ASC, f.file_path, f.line_number
539 | LIMIT 50
540 | """
541 | result = session.run(query, function_name=function_name)
542 | return result.data()
543 |
544 | def find_function_call_chain(self, start_function: str, end_function: str, max_depth: int = 5, start_file: str = None, end_file: str = None) -> List[Dict]:
545 | """Find call chains between two functions"""
546 | with self.driver.session() as session:
547 | # Build match clauses based on whether files are specified
548 | start_props = "{name: $start_function" + (", file_path: $start_file}" if start_file else "}")
549 | end_props = "{name: $end_function" + (", file_path: $end_file}" if end_file else "}")
550 |
551 | query = f"""
552 | MATCH (start:Function {start_props}), (end:Function {end_props})
553 | WITH start, end
554 | MATCH path = (start)-[:CALLS*1..{max_depth}]->(end)
555 | WHERE path IS NOT NULL
556 | WITH path, nodes(path) as func_nodes, relationships(path) as call_rels
557 | RETURN
558 | [node in func_nodes | {{
559 | name: node.name,
560 | file_path: node.file_path,
561 | line_number: node.line_number,
562 | is_dependency: node.is_dependency
563 | }}] as function_chain,
564 | [rel in call_rels | {{
565 | call_line: rel.line_number,
566 | args: rel.args,
567 | full_call_name: rel.full_call_name
568 | }}] as call_details,
569 | length(path) as chain_length
570 | ORDER BY chain_length ASC
571 | LIMIT 20
572 | """
573 |
574 | # Prepare parameters
575 | params = {
576 | "start_function": start_function,
577 | "end_function": end_function,
578 | "start_file": start_file,
579 | "end_file": end_file
580 | }
581 |
582 | result = session.run(query, **params)
583 | return result.data()
584 |
585 | def find_by_type(self, element_type: str, limit: int = 50) -> List[Dict]:
586 | """Find all elements of a specific type (Function, Class, File, Module)."""
587 | # Map input type to node label
588 | type_map = {
589 | "function": "Function",
590 | "class": "Class",
591 | "file": "File",
592 | "module": "Module"
593 | }
594 | label = type_map.get(element_type.lower())
595 |
596 | if not label:
597 | return []
598 |
599 | with self.driver.session() as session:
600 | if label == "File":
601 | query = f"""
602 | MATCH (n:File)
603 | RETURN n.name as name, n.path as file_path, n.is_dependency as is_dependency
604 | ORDER BY n.path
605 | LIMIT $limit
606 | """
607 | elif label == "Module":
608 | query = f"""
609 | MATCH (n:Module)
610 | RETURN n.name as name, n.name as file_path, false as is_dependency
611 | ORDER BY n.name
612 | LIMIT $limit
613 | """
614 | else:
615 | query = f"""
616 | MATCH (n:{label})
617 | RETURN n.name as name, n.file_path as file_path, n.line_number as line_number, n.is_dependency as is_dependency
618 | ORDER BY n.is_dependency ASC, n.name
619 | LIMIT $limit
620 | """
621 |
622 | result = session.run(query, limit=limit)
623 | return result.data()
624 |
625 | def find_module_dependencies(self, module_name: str) -> Dict[str, Any]:
626 | """Find all dependencies and dependents of a module"""
627 | with self.driver.session() as session:
628 | # Find files that import this module (who imports this module)
629 | importers_result = session.run("""
630 | MATCH (file:File)-[imp:IMPORTS]->(module:Module {name: $module_name})
631 | OPTIONAL MATCH (repo:Repository)-[:CONTAINS]->(file)
632 | RETURN DISTINCT
633 | file.path as importer_file_path,
634 | imp.line_number as import_line_number,
635 | file.is_dependency as file_is_dependency,
636 | repo.name as repository_name
637 | ORDER BY file.is_dependency ASC, file.path
638 | LIMIT 50
639 | """, module_name=module_name)
640 |
641 | # Find modules that are imported by files that also import the target module
642 | # This helps understand what this module is typically used with
643 | imports_result = session.run("""
644 | MATCH (file:File)-[:IMPORTS]->(target_module:Module {name: $module_name})
645 | MATCH (file)-[imp:IMPORTS]->(other_module:Module)
646 | WHERE other_module <> target_module
647 | RETURN DISTINCT
648 | other_module.name as imported_module,
649 | imp.alias as import_alias
650 | ORDER BY other_module.name
651 | LIMIT 50
652 | """, module_name=module_name)
653 |
654 | return {
655 | "module_name": module_name,
656 | "importers": [dict(record) for record in importers_result],
657 | "imports": [dict(record) for record in imports_result]
658 | }
659 |
660 | def find_variable_usage_scope(self, variable_name: str, file_path: str = None) -> Dict[str, Any]:
661 | """Find the scope and usage patterns of a variable, optional file path filtering"""
662 | with self.driver.session() as session:
663 | if file_path:
664 | variable_instances = session.run("""
665 | MATCH (var:Variable {name: $variable_name})
666 | WHERE var.file_path ENDS WITH $file_path OR var.file_path = $file_path
667 | OPTIONAL MATCH (container)-[:CONTAINS]->(var)
668 | WHERE container:Function OR container:Class OR container:File
669 | OPTIONAL MATCH (file:File)-[:CONTAINS]->(var)
670 | RETURN DISTINCT
671 | var.name as variable_name,
672 | var.value as variable_value,
673 | var.line_number as line_number,
674 | var.context as context,
675 | COALESCE(var.file_path, file.path) as file_path,
676 | CASE
677 | WHEN container:Function THEN 'function'
678 | WHEN container:Class THEN 'class'
679 | ELSE 'module'
680 | END as scope_type,
681 | CASE
682 | WHEN container:Function THEN container.name
683 | WHEN container:Class THEN container.name
684 | ELSE 'module_level'
685 | END as scope_name,
686 | var.is_dependency as is_dependency
687 | ORDER BY var.is_dependency ASC, file_path, line_number
688 | """, variable_name=variable_name, file_path=file_path)
689 | else:
690 | variable_instances = session.run("""
691 | MATCH (var:Variable {name: $variable_name})
692 | OPTIONAL MATCH (container)-[:CONTAINS]->(var)
693 | WHERE container:Function OR container:Class OR container:File
694 | OPTIONAL MATCH (file:File)-[:CONTAINS]->(var)
695 | RETURN DISTINCT
696 | var.name as variable_name,
697 | var.value as variable_value,
698 | var.line_number as line_number,
699 | var.context as context,
700 | COALESCE(var.file_path, file.path) as file_path,
701 | CASE
702 | WHEN container:Function THEN 'function'
703 | WHEN container:Class THEN 'class'
704 | ELSE 'module'
705 | END as scope_type,
706 | CASE
707 | WHEN container:Function THEN container.name
708 | WHEN container:Class THEN container.name
709 | ELSE 'module_level'
710 | END as scope_name,
711 | var.is_dependency as is_dependency
712 | ORDER BY var.is_dependency ASC, file_path, line_number
713 | """, variable_name=variable_name)
714 |
715 | return {
716 | "variable_name": variable_name,
717 | "instances": [dict(record) for record in variable_instances]
718 | }
719 |
720 | def analyze_code_relationships(self, query_type: str, target: str, context: str = None) -> Dict[str, Any]:
721 | """Main method to analyze different types of code relationships with fixed return types"""
722 | query_type = query_type.lower().strip()
723 |
724 | try:
725 | if query_type == "find_callers":
726 | results = self.who_calls_function(target, context)
727 | return {
728 | "query_type": "find_callers", "target": target, "context": context, "results": results,
729 | "summary": f"Found {len(results)} functions that call '{target}'"
730 | }
731 |
732 | elif query_type == "find_callees":
733 | results = self.what_does_function_call(target, context)
734 | return {
735 | "query_type": "find_callees", "target": target, "context": context, "results": results,
736 | "summary": f"Function '{target}' calls {len(results)} other functions"
737 | }
738 |
739 | elif query_type == "find_importers":
740 | results = self.who_imports_module(target)
741 | return {
742 | "query_type": "find_importers", "target": target, "results": results,
743 | "summary": f"Found {len(results)} files that import '{target}'"
744 | }
745 |
746 | elif query_type == "find_functions_by_argument":
747 | results = self.find_functions_by_argument(target, context)
748 | return {
749 | "query_type": "find_functions_by_argument", "target": target, "context": context, "results": results,
750 | "summary": f"Found {len(results)} functions that take '{target}' as an argument"
751 | }
752 |
753 | elif query_type == "find_functions_by_decorator":
754 | results = self.find_functions_by_decorator(target, context)
755 | return {
756 | "query_type": "find_functions_by_decorator", "target": target, "context": context, "results": results,
757 | "summary": f"Found {len(results)} functions decorated with '{target}'"
758 | }
759 |
760 | elif query_type in ["who_modifies", "modifies", "mutations", "changes", "variable_usage"]:
761 | results = self.who_modifies_variable(target)
762 | return {
763 | "query_type": "who_modifies", "target": target, "results": results,
764 | "summary": f"Found {len(results)} containers that hold variable '{target}'"
765 | }
766 |
767 | elif query_type in ["class_hierarchy", "inheritance", "extends"]:
768 | results = self.find_class_hierarchy(target, context)
769 | return {
770 | "query_type": "class_hierarchy", "target": target, "results": results,
771 | "summary": f"Class '{target}' has {len(results['parent_classes'])} parents, {len(results['child_classes'])} children, and {len(results['methods'])} methods"
772 | }
773 |
774 | elif query_type in ["overrides", "implementations", "polymorphism"]:
775 | results = self.find_function_overrides(target)
776 | return {
777 | "query_type": "overrides", "target": target, "results": results,
778 | "summary": f"Found {len(results)} implementations of function '{target}'"
779 | }
780 |
781 | elif query_type in ["dead_code", "unused", "unreachable"]:
782 | results = self.find_dead_code()
783 | return {
784 | "query_type": "dead_code", "results": results,
785 | "summary": f"Found {len(results['potentially_unused_functions'])} potentially unused functions"
786 | }
787 |
788 | elif query_type == "find_complexity":
789 | limit = int(context) if context and context.isdigit() else 10
790 | results = self.find_most_complex_functions(limit)
791 | return {
792 | "query_type": "find_complexity", "limit": limit, "results": results,
793 | "summary": f"Found the top {len(results)} most complex functions"
794 | }
795 |
796 | elif query_type == "find_all_callers":
797 | results = self.find_all_callers(target, context)
798 | return {
799 | "query_type": "find_all_callers", "target": target, "context": context, "results": results,
800 | "summary": f"Found {len(results)} direct and indirect callers of '{target}'"
801 | }
802 |
803 | elif query_type == "find_all_callees":
804 | results = self.find_all_callees(target, context)
805 | return {
806 | "query_type": "find_all_callees", "target": target, "context": context, "results": results,
807 | "summary": f"Found {len(results)} direct and indirect callees of '{target}'"
808 | }
809 |
810 | elif query_type in ["call_chain", "path", "chain"]:
811 | if '->' in target:
812 | start_func, end_func = target.split('->', 1)
813 | # max_depth can be passed as context, default to 5 if not provided or invalid
814 | max_depth = int(context) if context and context.isdigit() else 5
815 | results = self.find_function_call_chain(start_func.strip(), end_func.strip(), max_depth)
816 | return {
817 | "query_type": "call_chain", "target": target, "results": results,
818 | "summary": f"Found {len(results)} call chains from '{start_func.strip()}' to '{end_func.strip()}' (max depth: {max_depth})"
819 | }
820 | else:
821 | return {
822 | "error": "For call_chain queries, use format 'start_function->end_function'",
823 | "example": "main->process_data"
824 | }
825 |
826 | elif query_type in ["module_deps", "module_dependencies", "module_usage"]:
827 | results = self.find_module_dependencies(target)
828 | return {
829 | "query_type": "module_dependencies", "target": target, "results": results,
830 | "summary": f"Module '{target}' is imported by {len(results['imported_by_files'])} files"
831 | }
832 |
833 | elif query_type in ["variable_scope", "var_scope", "variable_usage_scope"]:
834 | results = self.find_variable_usage_scope(target)
835 | return {
836 | "query_type": "variable_scope", "target": target, "results": results,
837 | "summary": f"Variable '{target}' has {len(results['instances'])} instances across different scopes"
838 | }
839 |
840 | else:
841 | return {
842 | "error": f"Unknown query type: {query_type}",
843 | "supported_types": [
844 | "find_callers", "find_callees", "find_importers", "who_modifies",
845 | "class_hierarchy", "overrides", "dead_code", "call_chain",
846 | "module_deps", "variable_scope", "find_complexity"
847 | ]
848 | }
849 |
850 | except Exception as e:
851 | return {
852 | "error": f"Error executing relationship query: {str(e)}",
853 | "query_type": query_type,
854 | "target": target
855 | }
856 |
857 | def get_cyclomatic_complexity(self, function_name: str, file_path: str = None) -> Optional[Dict]:
858 | """Get the cyclomatic complexity of a function."""
859 | with self.driver.session() as session:
860 | if file_path:
861 | # Use ENDS WITH for flexible path matching, or exact match
862 | query = """
863 | MATCH (f:Function {name: $function_name})
864 | WHERE f.file_path ENDS WITH $file_path OR f.file_path = $file_path
865 | RETURN f.name as function_name, f.cyclomatic_complexity as complexity,
866 | f.file_path as file_path, f.line_number as line_number
867 | """
868 | result = session.run(query, function_name=function_name, file_path=file_path)
869 | else:
870 | query = """
871 | MATCH (f:Function {name: $function_name})
872 | RETURN f.name as function_name, f.cyclomatic_complexity as complexity,
873 | f.file_path as file_path, f.line_number as line_number
874 | """
875 | result = session.run(query, function_name=function_name)
876 |
877 | result_data = result.data()
878 | if result_data:
879 | return result_data[0]
880 | return None
881 |
882 | def find_most_complex_functions(self, limit: int = 10) -> List[Dict]:
883 | """Find the most complex functions based on cyclomatic complexity."""
884 | with self.driver.session() as session:
885 | query = """
886 | MATCH (f:Function)
887 | WHERE f.cyclomatic_complexity IS NOT NULL AND f.is_dependency = false
888 | RETURN f.name as function_name, f.file_path as file_path, f.cyclomatic_complexity as complexity, f.line_number as line_number
889 | ORDER BY f.cyclomatic_complexity DESC
890 | LIMIT $limit
891 | """
892 | result = session.run(query, limit=limit)
893 | return result.data()
894 |
895 | def list_indexed_repositories(self) -> List[Dict]:
896 | """List all indexed repositories."""
897 | with self.driver.session() as session:
898 | result = session.run("""
899 | MATCH (r:Repository)
900 | RETURN r.name as name, r.path as path, r.is_dependency as is_dependency
901 | ORDER BY r.name
902 | """)
903 | return result.data()
904 |
```