This is page 14 of 18. Use http://codebase.md/shashankss1205/codegraphcontext?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .github
│ └── workflows
│ ├── e2e-tests.yml
│ ├── post_discord_invite.yml
│ ├── test.yml
│ └── update-contributors.yml
├── .gitignore
├── 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
│ ├── mkdocs.yml
│ └── site
│ ├── 404.html
│ ├── architecture
│ │ └── index.html
│ ├── assets
│ │ ├── images
│ │ │ └── favicon.png
│ │ ├── javascripts
│ │ │ ├── bundle.f55a23d4.min.js
│ │ │ ├── bundle.f55a23d4.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.973d3a69.min.js
│ │ │ └── search.973d3a69.min.js.map
│ │ └── stylesheets
│ │ ├── main.2a3383ac.min.css
│ │ ├── main.2a3383ac.min.css.map
│ │ ├── palette.06af60db.min.css
│ │ └── palette.06af60db.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
├── 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
├── package-lock.json
├── pyproject.toml
├── README.md
├── scripts
│ ├── generate_lang_contributors.py
│ └── post_install_fix.sh
├── SECURITY.md
├── src
│ └── codegraphcontext
│ ├── __init__.py
│ ├── __main__.py
│ ├── cli
│ │ ├── __init__.py
│ │ ├── cli_helpers.py
│ │ ├── main.py
│ │ ├── setup_macos.py
│ │ └── setup_wizard.py
│ ├── core
│ │ ├── __init__.py
│ │ ├── database.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
│ │ │ ├── go.py
│ │ │ ├── java.py
│ │ │ ├── javascript.py
│ │ │ ├── python.py
│ │ │ ├── ruby.py
│ │ │ ├── rust.py
│ │ │ └── typescript.py
│ │ ├── package_resolver.py
│ │ ├── query_tool_languages
│ │ │ ├── c_toolkit.py
│ │ │ ├── cpp_toolkit.py
│ │ │ ├── go_toolkit.py
│ │ │ ├── java_toolkit.py
│ │ │ ├── javascript_toolkit.py
│ │ │ ├── python_toolkit.py
│ │ │ ├── ruby_toolkit.py
│ │ │ ├── rust_toolkit.py
│ │ │ └── typescript_toolkit.py
│ │ └── system.py
│ └── utils
│ └── debug_log.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_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_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_typescript
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── 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_tree_sitter
│ │ ├── __init__.py
│ │ ├── class_instantiation.py
│ │ ├── complex_classes.py
│ │ └── test_file.py
│ └── test_typescript_parser.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
│ │ ├── 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
```
# Files
--------------------------------------------------------------------------------
/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
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 using the full-text index."""
32 | with self.driver.session() as session:
33 | if fuzzy_search:
34 | formatted_search_term = f"name:{search_term}"
35 | result = session.run(self.format_query("Function", fuzzy_search), search_term=formatted_search_term)
36 | else:
37 | result = session.run(self.format_query("Function", fuzzy_search), search_term=search_term)
38 | return [dict(record) for record in result]
39 |
40 | def find_by_class_name(self, search_term: str, fuzzy_search: bool) -> List[Dict]:
41 | """Find classes by name matching using the full-text index."""
42 | with self.driver.session() as session:
43 | if fuzzy_search:
44 | formatted_search_term = f"name:{search_term}"
45 | result = session.run(self.format_query("Class", fuzzy_search), search_term=formatted_search_term)
46 | else:
47 | result = session.run(self.format_query("Class", fuzzy_search), search_term=search_term)
48 | return [dict(record) for record in result]
49 |
50 | def find_by_variable_name(self, search_term: str) -> List[Dict]:
51 | """Find variables by name matching"""
52 | with self.driver.session() as session:
53 | result = session.run("""
54 | MATCH (v:Variable)
55 | WHERE v.name CONTAINS $search_term OR v.name =~ $regex_pattern
56 | RETURN v.name as name, v.file_path as file_path, v.line_number as line_number,
57 | v.value as value, v.context as context, v.is_dependency as is_dependency
58 | ORDER BY v.is_dependency ASC, v.name
59 | LIMIT 20
60 | """, search_term=search_term, regex_pattern=f"(?i).*{re.escape(search_term)}.*")
61 |
62 | return [dict(record) for record in result]
63 |
64 | def find_by_content(self, search_term: str) -> List[Dict]:
65 | """Find code by content matching in source or docstrings using the full-text index."""
66 | with self.driver.session() as session:
67 | result = session.run("""
68 | CALL db.index.fulltext.queryNodes("code_search_index", $search_term) YIELD node, score
69 | WITH node, score
70 | WHERE node:Function OR node:Class OR node:Variable
71 | RETURN
72 | CASE
73 | WHEN node:Function THEN 'function'
74 | WHEN node:Class THEN 'class'
75 | ELSE 'variable'
76 | END as type,
77 | node.name as name, node.file_path as file_path,
78 | node.line_number as line_number, node.source as source,
79 | node.docstring as docstring, node.is_dependency as is_dependency
80 | ORDER BY score DESC
81 | LIMIT 20
82 | """, search_term=search_term)
83 | return [dict(record) for record in result]
84 |
85 | def find_related_code(self, user_query: str, fuzzy_search: bool, edit_distance: int) -> Dict[str, Any]:
86 | """Find code related to a query using multiple search strategies"""
87 | if fuzzy_search:
88 | user_query_normalized = " ".join(map(lambda x: f"{x}~{edit_distance}", user_query.split(" ")))
89 | else:
90 | user_query_normalized = user_query
91 |
92 | results = {
93 | "query": user_query_normalized,
94 | "functions_by_name": self.find_by_function_name(user_query_normalized, fuzzy_search),
95 | "classes_by_name": self.find_by_class_name(user_query_normalized, fuzzy_search),
96 | "variables_by_name": self.find_by_variable_name(user_query), # no fuzzy for variables as they are not using full-text index
97 | "content_matches": self.find_by_content(user_query_normalized)
98 | }
99 |
100 | all_results = []
101 |
102 | for func in results["functions_by_name"]:
103 | func["search_type"] = "function_name"
104 | func["relevance_score"] = 0.9 if not func["is_dependency"] else 0.7
105 | all_results.append(func)
106 |
107 | for cls in results["classes_by_name"]:
108 | cls["search_type"] = "class_name"
109 | cls["relevance_score"] = 0.8 if not cls["is_dependency"] else 0.6
110 | all_results.append(cls)
111 |
112 | for var in results["variables_by_name"]:
113 | var["search_type"] = "variable_name"
114 | var["relevance_score"] = 0.7 if not var["is_dependency"] else 0.5
115 | all_results.append(var)
116 |
117 | for content in results["content_matches"]:
118 | content["search_type"] = "content"
119 | content["relevance_score"] = 0.6 if not content["is_dependency"] else 0.4
120 | all_results.append(content)
121 |
122 | all_results.sort(key=lambda x: x["relevance_score"], reverse=True)
123 |
124 | results["ranked_results"] = all_results[:15]
125 | results["total_matches"] = len(all_results)
126 |
127 | return results
128 |
129 | def find_functions_by_argument(self, argument_name: str, file_path: str = None) -> List[Dict]:
130 | """Find functions that take a specific argument name."""
131 | with self.driver.session() as session:
132 | if file_path:
133 | query = """
134 | MATCH (f:Function)-[:HAS_PARAMETER]->(p:Parameter)
135 | WHERE p.name = $argument_name AND f.file_path = $file_path
136 | RETURN f.name AS function_name, f.file_path AS file_path, f.line_number AS line_number,
137 | f.docstring AS docstring, f.is_dependency AS is_dependency
138 | ORDER BY f.is_dependency ASC, f.file_path, f.line_number
139 | LIMIT 20
140 | """
141 | result = session.run(query, argument_name=argument_name, file_path=file_path)
142 | else:
143 | query = """
144 | MATCH (f:Function)-[:HAS_PARAMETER]->(p:Parameter)
145 | WHERE p.name = $argument_name
146 | RETURN f.name AS function_name, f.file_path AS file_path, f.line_number AS line_number,
147 | f.docstring AS docstring, f.is_dependency AS is_dependency
148 | ORDER BY f.is_dependency ASC, f.file_path, f.line_number
149 | LIMIT 20
150 | """
151 | result = session.run(query, argument_name=argument_name)
152 | return [dict(record) for record in result]
153 |
154 | def find_functions_by_decorator(self, decorator_name: str, file_path: str = None) -> List[Dict]:
155 | """Find functions that have a specific decorator applied to them."""
156 | with self.driver.session() as session:
157 | if file_path:
158 | query = """
159 | MATCH (f:Function)
160 | WHERE f.file_path = $file_path AND $decorator_name IN f.decorators
161 | RETURN f.name AS function_name, f.file_path AS file_path, f.line_number AS line_number,
162 | f.docstring AS docstring, f.is_dependency AS is_dependency, f.decorators AS decorators
163 | ORDER BY f.is_dependency ASC, f.file_path, f.line_number
164 | LIMIT 20
165 | """
166 | result = session.run(query, decorator_name=decorator_name, file_path=file_path)
167 | else:
168 | query = """
169 | MATCH (f:Function)
170 | WHERE $decorator_name IN f.decorators
171 | RETURN f.name AS function_name, f.file_path AS file_path, f.line_number AS line_number,
172 | f.docstring AS docstring, f.is_dependency AS is_dependency, f.decorators AS decorators
173 | ORDER BY f.is_dependency ASC, f.file_path, f.line_number
174 | LIMIT 20
175 | """
176 | result = session.run(query, decorator_name=decorator_name)
177 | return [dict(record) for record in result]
178 |
179 | def who_calls_function(self, function_name: str, file_path: str = None) -> List[Dict]:
180 | """Find what functions call a specific function using CALLS relationships with improved matching"""
181 | with self.driver.session() as session:
182 | if file_path:
183 | result = session.run("""
184 | MATCH (caller:Function)-[call:CALLS]->(target:Function {name: $function_name, file_path: $file_path})
185 | OPTIONAL MATCH (caller_file:File)-[:CONTAINS]->(caller)
186 | RETURN DISTINCT
187 | caller.name as caller_function,
188 | caller.file_path as caller_file_path,
189 | caller.line_number as caller_line_number,
190 | caller.docstring as caller_docstring,
191 | caller.is_dependency as caller_is_dependency,
192 | call.line_number as call_line_number,
193 | call.args as call_args,
194 | call.full_call_name as full_call_name,
195 | call.call_type as call_type,
196 | target.file_path as target_file_path
197 | ORDER BY caller.is_dependency ASC, caller.file_path, caller.line_number
198 | LIMIT 20
199 | """, function_name=function_name, file_path=file_path)
200 |
201 | results = [dict(record) for record in result]
202 | if not results:
203 | result = session.run("""
204 | MATCH (target:Function {name: $function_name})
205 | MATCH (caller:Function)-[call:CALLS]->(target)
206 | OPTIONAL MATCH (caller_file:File)-[:CONTAINS]->(caller)
207 | RETURN DISTINCT
208 | caller.name as caller_function,
209 | caller.file_path as caller_file_path,
210 | caller.line_number as caller_line_number,
211 | caller.docstring as caller_docstring,
212 | caller.is_dependency as caller_is_dependency,
213 | call.line_number as call_line_number,
214 | call.args as call_args,
215 | call.full_call_name as full_call_name,
216 | call.call_type as call_type,
217 | target.file_path as target_file_path
218 | ORDER BY caller.is_dependency ASC, caller.file_path, caller.line_number
219 | LIMIT 20
220 | """, function_name=function_name)
221 | results = [dict(record) for record in result]
222 | else:
223 | result = session.run("""
224 | MATCH (target:Function {name: $function_name})
225 | MATCH (caller:Function)-[call:CALLS]->(target)
226 | OPTIONAL MATCH (caller_file:File)-[:CONTAINS]->(caller)
227 | RETURN DISTINCT
228 | caller.name as caller_function,
229 | caller.file_path as caller_file_path,
230 | caller.line_number as caller_line_number,
231 | caller.docstring as caller_docstring,
232 | caller.is_dependency as caller_is_dependency,
233 | call.line_number as call_line_number,
234 | call.args as call_args,
235 | call.full_call_name as full_call_name,
236 | call.call_type as call_type,
237 | target.file_path as target_file_path
238 | ORDER BY caller.is_dependency ASC, caller.file_path, caller.line_number
239 | LIMIT 20
240 | """, function_name=function_name)
241 | results = [dict(record) for record in result]
242 |
243 | return results
244 |
245 | def what_does_function_call(self, function_name: str, file_path: str = None) -> List[Dict]:
246 | """Find what functions a specific function calls using CALLS relationships"""
247 | with self.driver.session() as session:
248 | if file_path:
249 | # Convert file_path to absolute path
250 | absolute_file_path = str(Path(file_path).resolve())
251 | result = session.run("""
252 | MATCH (caller:Function {name: $function_name, file_path: $absolute_file_path})
253 | MATCH (caller)-[call:CALLS]->(called:Function)
254 | OPTIONAL MATCH (called_file:File)-[:CONTAINS]->(called)
255 | RETURN DISTINCT
256 | called.name as called_function,
257 | called.file_path as called_file_path,
258 | called.line_number as called_line_number,
259 | called.docstring as called_docstring,
260 | called.is_dependency as called_is_dependency,
261 | call.line_number as call_line_number,
262 | call.args as call_args,
263 | call.full_call_name as full_call_name,
264 | call.call_type as call_type
265 | ORDER BY called.is_dependency ASC, called.name
266 | LIMIT 20
267 | """, function_name=function_name, absolute_file_path=absolute_file_path)
268 | else:
269 | result = session.run("""
270 | MATCH (caller:Function {name: $function_name})
271 | MATCH (caller)-[call:CALLS]->(called:Function)
272 | OPTIONAL MATCH (called_file:File)-[:CONTAINS]->(called)
273 | RETURN DISTINCT
274 | called.name as called_function,
275 | called.file_path as called_file_path,
276 | called.line_number as called_line_number,
277 | called.docstring as called_docstring,
278 | called.is_dependency as called_is_dependency,
279 | call.line_number as call_line_number,
280 | call.args as call_args,
281 | call.full_call_name as full_call_name,
282 | call.call_type as call_type
283 | ORDER BY called.is_dependency ASC, called.name
284 | LIMIT 20
285 | """, function_name=function_name)
286 |
287 | return [dict(record) for record in result]
288 |
289 | def who_imports_module(self, module_name: str) -> List[Dict]:
290 | """Find what files import a specific module using IMPORTS relationships"""
291 | with self.driver.session() as session:
292 | result = session.run("""
293 | MATCH (file:File)-[imp:IMPORTS]->(module:Module)
294 | WHERE module.name = $module_name OR module.full_import_name CONTAINS $module_name
295 | OPTIONAL MATCH (repo:Repository)-[:CONTAINS]->(file)
296 | WITH file, repo, COLLECT({
297 | imported_module: module.name,
298 | import_alias: module.alias,
299 | full_import_name: module.full_import_name
300 | }) AS imports
301 | RETURN
302 | file.name AS file_name,
303 | file.path AS file_path,
304 | file.relative_path AS file_relative_path,
305 | file.is_dependency AS file_is_dependency,
306 | repo.name AS repository_name,
307 | imports
308 | ORDER BY file.is_dependency ASC, file.path
309 | LIMIT 20
310 | """, module_name=module_name)
311 |
312 | return [dict(record) for record in result]
313 |
314 | def who_modifies_variable(self, variable_name: str) -> List[Dict]:
315 | """Find what functions contain or modify a specific variable"""
316 | with self.driver.session() as session:
317 | result = session.run("""
318 | MATCH (var:Variable {name: $variable_name})
319 | MATCH (container)-[:CONTAINS]->(var)
320 | WHERE container:Function OR container:Class OR container:File
321 | OPTIONAL MATCH (file:File)-[:CONTAINS]->(container)
322 | RETURN DISTINCT
323 | CASE
324 | WHEN container:Function THEN container.name
325 | WHEN container:Class THEN container.name
326 | ELSE 'file_level'
327 | END as container_name,
328 | CASE
329 | WHEN container:Function THEN 'function'
330 | WHEN container:Class THEN 'class'
331 | ELSE 'file'
332 | END as container_type,
333 | COALESCE(container.file_path, file.path) as file_path,
334 | container.line_number as container_line_number,
335 | var.line_number as variable_line_number,
336 | var.value as variable_value,
337 | var.context as variable_context,
338 | COALESCE(container.is_dependency, file.is_dependency, false) as is_dependency
339 | ORDER BY is_dependency ASC, file_path, variable_line_number
340 | LIMIT 20
341 | """, variable_name=variable_name)
342 |
343 | return [dict(record) for record in result]
344 |
345 | def find_class_hierarchy(self, class_name: str, file_path: str = None) -> Dict[str, Any]:
346 | """Find class inheritance relationships using INHERITS relationships"""
347 | with self.driver.session() as session:
348 | if file_path:
349 | match_clause = "MATCH (child:Class {name: $class_name, file_path: $file_path})"
350 | else:
351 | match_clause = "MATCH (child:Class {name: $class_name})"
352 |
353 | parents_query = f"""
354 | {match_clause}
355 | MATCH (child)-[:INHERITS]->(parent:Class)
356 | OPTIONAL MATCH (parent_file:File)-[:CONTAINS]->(parent)
357 | RETURN DISTINCT
358 | parent.name as parent_class,
359 | parent.file_path as parent_file_path,
360 | parent.line_number as parent_line_number,
361 | parent.docstring as parent_docstring,
362 | parent.is_dependency as parent_is_dependency
363 | ORDER BY parent.is_dependency ASC, parent.name
364 | """
365 | parents_result = session.run(parents_query, class_name=class_name, file_path=file_path)
366 |
367 | children_query = f"""
368 | {match_clause}
369 | MATCH (grandchild:Class)-[:INHERITS]->(child)
370 | OPTIONAL MATCH (child_file:File)-[:CONTAINS]->(grandchild)
371 | RETURN DISTINCT
372 | grandchild.name as child_class,
373 | grandchild.file_path as child_file_path,
374 | grandchild.line_number as child_line_number,
375 | grandchild.docstring as child_docstring,
376 | grandchild.is_dependency as child_is_dependency
377 | ORDER BY grandchild.is_dependency ASC, grandchild.name
378 | """
379 | children_result = session.run(children_query, class_name=class_name, file_path=file_path)
380 |
381 | methods_query = f"""
382 | {match_clause}
383 | MATCH (child)-[:CONTAINS]->(method:Function)
384 | RETURN DISTINCT
385 | method.name as method_name,
386 | method.file_path as method_file_path,
387 | method.line_number as method_line_number,
388 | method.args as method_args,
389 | method.docstring as method_docstring,
390 | method.is_dependency as method_is_dependency
391 | ORDER BY method.is_dependency ASC, method.line_number
392 | """
393 | methods_result = session.run(methods_query, class_name=class_name, file_path=file_path)
394 |
395 | return {
396 | "class_name": class_name,
397 | "parent_classes": [dict(record) for record in parents_result],
398 | "child_classes": [dict(record) for record in children_result],
399 | "methods": [dict(record) for record in methods_result]
400 | }
401 |
402 | def find_function_overrides(self, function_name: str) -> List[Dict]:
403 | """Find all implementations of a function across different classes"""
404 | with self.driver.session() as session:
405 | result = session.run("""
406 | MATCH (class:Class)-[:CONTAINS]->(func:Function {name: $function_name})
407 | OPTIONAL MATCH (file:File)-[:CONTAINS]->(class)
408 | RETURN DISTINCT
409 | class.name as class_name,
410 | class.file_path as class_file_path,
411 | func.name as function_name,
412 | func.line_number as function_line_number,
413 | func.args as function_args,
414 | func.docstring as function_docstring,
415 | func.is_dependency as is_dependency,
416 | file.name as file_name
417 | ORDER BY func.is_dependency ASC, class.name
418 | LIMIT 20
419 | """, function_name=function_name)
420 |
421 | return [dict(record) for record in result]
422 |
423 | def find_dead_code(self, exclude_decorated_with: List[str] = None) -> Dict[str, Any]:
424 | """Find potentially unused functions (not called by other functions in the project), optionally excluding those with specific decorators."""
425 | if exclude_decorated_with is None:
426 | exclude_decorated_with = []
427 |
428 | with self.driver.session() as session:
429 | result = session.run("""
430 | MATCH (func:Function)
431 | WHERE func.is_dependency = false
432 | AND NOT func.name IN ['main', '__init__', '__main__', 'setup', 'run', '__new__', '__del__']
433 | AND NOT func.name STARTS WITH '_test'
434 | AND NOT func.name STARTS WITH 'test_'
435 | AND ALL(decorator_name IN $exclude_decorated_with WHERE NOT decorator_name IN func.decorators)
436 | WITH func
437 | OPTIONAL MATCH (caller:Function)-[:CALLS]->(func)
438 | WHERE caller.is_dependency = false
439 | WITH func, count(caller) as caller_count
440 | WHERE caller_count = 0
441 | OPTIONAL MATCH (file:File)-[:CONTAINS]->(func)
442 | RETURN
443 | func.name as function_name,
444 | func.file_path as file_path,
445 | func.line_number as line_number,
446 | func.docstring as docstring,
447 | func.context as context,
448 | file.name as file_name
449 | ORDER BY func.file_path, func.line_number
450 | LIMIT 50
451 | """, exclude_decorated_with=exclude_decorated_with)
452 |
453 | return {
454 | "potentially_unused_functions": [dict(record) for record in result],
455 | "note": "These functions might be unused, but could be entry points, callbacks, or called dynamically"
456 | }
457 |
458 | def find_all_callers(self, function_name: str, file_path: str = None) -> List[Dict]:
459 | """Find all direct and indirect callers of a specific function."""
460 | with self.driver.session() as session:
461 | if file_path:
462 | # Find functions within the specified file_path that call the target function
463 | query = """
464 | MATCH (f:Function)-[:CALLS*]->(target:Function {name: $function_name, file_path: $file_path})
465 | 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
466 | ORDER BY f.is_dependency ASC, f.file_path, f.line_number
467 | LIMIT 50
468 | """
469 | result = session.run(query, function_name=function_name, file_path=file_path)
470 | else:
471 | # If no file_path (context) is provided, find all callers of the function by name
472 | query = """
473 | MATCH (f:Function)-[:CALLS*]->(target:Function {name: $function_name})
474 | 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
475 | ORDER BY f.is_dependency ASC, f.file_path, f.line_number
476 | LIMIT 50
477 | """
478 | result = session.run(query, function_name=function_name)
479 | return [dict(record) for record in result]
480 |
481 | def find_all_callees(self, function_name: str, file_path: str = None) -> List[Dict]:
482 | """Find all direct and indirect callees of a specific function."""
483 | with self.driver.session() as session:
484 | if file_path:
485 | query = """
486 | MATCH (caller:Function {name: $function_name, file_path: $file_path})
487 | MATCH (caller)-[:CALLS*]->(f:Function)
488 | 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
489 | ORDER BY f.is_dependency ASC, f.file_path, f.line_number
490 | LIMIT 50
491 | """
492 | result = session.run(query, function_name=function_name, file_path=file_path)
493 | else:
494 | query = """
495 | MATCH (caller:Function {name: $function_name})
496 | MATCH (caller)-[:CALLS*]->(f:Function)
497 | 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
498 | ORDER BY f.is_dependency ASC, f.file_path, f.line_number
499 | LIMIT 50
500 | """
501 | result = session.run(query, function_name=function_name)
502 | return [dict(record) for record in result]
503 |
504 | def find_function_call_chain(self, start_function: str, end_function: str, max_depth: int = 5) -> List[Dict]:
505 | """Find call chains between two functions"""
506 | with self.driver.session() as session:
507 | result = session.run(f"""
508 | MATCH path = shortestPath(
509 | (start:Function {{name: $start_function}})-[:CALLS*1..{max_depth}]->(end:Function {{name: $end_function}})
510 | )
511 | WITH path, nodes(path) as func_nodes, relationships(path) as call_rels
512 | RETURN
513 | [node in func_nodes | {{
514 | name: node.name,
515 | file_path: node.file_path,
516 | line_number: node.line_number,
517 | is_dependency: node.is_dependency
518 | }}] as function_chain,
519 | [rel in call_rels | {{
520 | call_line: rel.line_number,
521 | args: rel.args,
522 | full_call_name: rel.full_call_name
523 | }}] as call_details,
524 | length(path) as chain_length
525 | ORDER BY chain_length ASC
526 | LIMIT 10
527 | """, start_function=start_function, end_function=end_function)
528 |
529 | return [dict(record) for record in result]
530 |
531 | def find_module_dependencies(self, module_name: str) -> Dict[str, Any]:
532 | """Find all dependencies and dependents of a module"""
533 | with self.driver.session() as session:
534 | importers_result = session.run("""
535 | MATCH (file:File)-[:IMPORTS]->(module:Module {name: $module_name})
536 | OPTIONAL MATCH (repo:Repository)-[:CONTAINS]->(file)
537 | RETURN DISTINCT
538 | file.name as file_name,
539 | file.path as file_path,
540 | file.is_dependency as file_is_dependency,
541 | repo.name as repository_name
542 | ORDER BY file.is_dependency ASC, file.path
543 | LIMIT 20
544 | """, module_name=module_name)
545 |
546 | related_imports_result = session.run("""
547 | MATCH (file:File)-[:IMPORTS]->(target_module:Module {name: $module_name})
548 | MATCH (file)-[:IMPORTS]->(other_module:Module)
549 | WHERE other_module <> target_module
550 | RETURN DISTINCT
551 | other_module.name as related_module,
552 | other_module.alias as module_alias,
553 | count(file) as usage_count
554 | ORDER BY usage_count DESC
555 | LIMIT 20
556 | """, module_name=module_name)
557 |
558 | return {
559 | "module_name": module_name,
560 | "imported_by_files": [dict(record) for record in importers_result],
561 | "frequently_used_with": [dict(record) for record in related_imports_result]
562 | }
563 |
564 | def find_variable_usage_scope(self, variable_name: str) -> Dict[str, Any]:
565 | """Find the scope and usage patterns of a variable"""
566 | with self.driver.session() as session:
567 | variable_instances = session.run("""
568 | MATCH (var:Variable {name: $variable_name})
569 | OPTIONAL MATCH (container)-[:CONTAINS]->(var)
570 | WHERE container:Function OR container:Class OR container:File
571 | OPTIONAL MATCH (file:File)-[:CONTAINS]->(var)
572 | RETURN DISTINCT
573 | var.name as variable_name,
574 | var.value as variable_value,
575 | var.line_number as line_number,
576 | var.context as context,
577 | COALESCE(var.file_path, file.path) as file_path,
578 | CASE
579 | WHEN container:Function THEN 'function'
580 | WHEN container:Class THEN 'class'
581 | ELSE 'module'
582 | END as scope_type,
583 | CASE
584 | WHEN container:Function THEN container.name
585 | WHEN container:Class THEN container.name
586 | ELSE 'module_level'
587 | END as scope_name,
588 | var.is_dependency as is_dependency
589 | ORDER BY var.is_dependency ASC, file_path, line_number
590 | """, variable_name=variable_name)
591 |
592 | return {
593 | "variable_name": variable_name,
594 | "instances": [dict(record) for record in variable_instances]
595 | }
596 |
597 | def analyze_code_relationships(self, query_type: str, target: str, context: str = None) -> Dict[str, Any]:
598 | """Main method to analyze different types of code relationships with fixed return types"""
599 | query_type = query_type.lower().strip()
600 |
601 | try:
602 | if query_type == "find_callers":
603 | results = self.who_calls_function(target, context)
604 | return {
605 | "query_type": "find_callers", "target": target, "context": context, "results": results,
606 | "summary": f"Found {len(results)} functions that call '{target}'"
607 | }
608 |
609 | elif query_type == "find_callees":
610 | results = self.what_does_function_call(target, context)
611 | return {
612 | "query_type": "find_callees", "target": target, "context": context, "results": results,
613 | "summary": f"Function '{target}' calls {len(results)} other functions"
614 | }
615 |
616 | elif query_type == "find_importers":
617 | results = self.who_imports_module(target)
618 | return {
619 | "query_type": "find_importers", "target": target, "results": results,
620 | "summary": f"Found {len(results)} files that import '{target}'"
621 | }
622 |
623 | elif query_type == "find_functions_by_argument":
624 | results = self.find_functions_by_argument(target, context)
625 | return {
626 | "query_type": "find_functions_by_argument", "target": target, "context": context, "results": results,
627 | "summary": f"Found {len(results)} functions that take '{target}' as an argument"
628 | }
629 |
630 | elif query_type == "find_functions_by_decorator":
631 | results = self.find_functions_by_decorator(target, context)
632 | return {
633 | "query_type": "find_functions_by_decorator", "target": target, "context": context, "results": results,
634 | "summary": f"Found {len(results)} functions decorated with '{target}'"
635 | }
636 |
637 | elif query_type in ["who_modifies", "modifies", "mutations", "changes", "variable_usage"]:
638 | results = self.who_modifies_variable(target)
639 | return {
640 | "query_type": "who_modifies", "target": target, "results": results,
641 | "summary": f"Found {len(results)} containers that hold variable '{target}'"
642 | }
643 |
644 | elif query_type in ["class_hierarchy", "inheritance", "extends"]:
645 | results = self.find_class_hierarchy(target, context)
646 | return {
647 | "query_type": "class_hierarchy", "target": target, "results": results,
648 | "summary": f"Class '{target}' has {len(results['parent_classes'])} parents, {len(results['child_classes'])} children, and {len(results['methods'])} methods"
649 | }
650 |
651 | elif query_type in ["overrides", "implementations", "polymorphism"]:
652 | results = self.find_function_overrides(target)
653 | return {
654 | "query_type": "overrides", "target": target, "results": results,
655 | "summary": f"Found {len(results)} implementations of function '{target}'"
656 | }
657 |
658 | elif query_type in ["dead_code", "unused", "unreachable"]:
659 | results = self.find_dead_code()
660 | return {
661 | "query_type": "dead_code", "results": results,
662 | "summary": f"Found {len(results['potentially_unused_functions'])} potentially unused functions"
663 | }
664 |
665 | elif query_type == "find_complexity":
666 | limit = int(context) if context and context.isdigit() else 10
667 | results = self.find_most_complex_functions(limit)
668 | return {
669 | "query_type": "find_complexity", "limit": limit, "results": results,
670 | "summary": f"Found the top {len(results)} most complex functions"
671 | }
672 |
673 | elif query_type == "find_all_callers":
674 | results = self.find_all_callers(target, context)
675 | return {
676 | "query_type": "find_all_callers", "target": target, "context": context, "results": results,
677 | "summary": f"Found {len(results)} direct and indirect callers of '{target}'"
678 | }
679 |
680 | elif query_type == "find_all_callees":
681 | results = self.find_all_callees(target, context)
682 | return {
683 | "query_type": "find_all_callees", "target": target, "context": context, "results": results,
684 | "summary": f"Found {len(results)} direct and indirect callees of '{target}'"
685 | }
686 |
687 | elif query_type in ["call_chain", "path", "chain"]:
688 | if '->' in target:
689 | start_func, end_func = target.split('->', 1)
690 | # max_depth can be passed as context, default to 5 if not provided or invalid
691 | max_depth = int(context) if context and context.isdigit() else 5
692 | results = self.find_function_call_chain(start_func.strip(), end_func.strip(), max_depth)
693 | return {
694 | "query_type": "call_chain", "target": target, "results": results,
695 | "summary": f"Found {len(results)} call chains from '{start_func.strip()}' to '{end_func.strip()}' (max depth: {max_depth})"
696 | }
697 | else:
698 | return {
699 | "error": "For call_chain queries, use format 'start_function->end_function'",
700 | "example": "main->process_data"
701 | }
702 |
703 | elif query_type in ["module_deps", "module_dependencies", "module_usage"]:
704 | results = self.find_module_dependencies(target)
705 | return {
706 | "query_type": "module_dependencies", "target": target, "results": results,
707 | "summary": f"Module '{target}' is imported by {len(results['imported_by_files'])} files"
708 | }
709 |
710 | elif query_type in ["variable_scope", "var_scope", "variable_usage_scope"]:
711 | results = self.find_variable_usage_scope(target)
712 | return {
713 | "query_type": "variable_scope", "target": target, "results": results,
714 | "summary": f"Variable '{target}' has {len(results['instances'])} instances across different scopes"
715 | }
716 |
717 | else:
718 | return {
719 | "error": f"Unknown query type: {query_type}",
720 | "supported_types": [
721 | "find_callers", "find_callees", "find_importers", "who_modifies",
722 | "class_hierarchy", "overrides", "dead_code", "call_chain",
723 | "module_deps", "variable_scope", "find_complexity"
724 | ]
725 | }
726 |
727 | except Exception as e:
728 | return {
729 | "error": f"Error executing relationship query: {str(e)}",
730 | "query_type": query_type,
731 | "target": target
732 | }
733 |
734 | def get_cyclomatic_complexity(self, function_name: str, file_path: str = None) -> List[Dict]:
735 | """Get the cyclomatic complexity of a function."""
736 | with self.driver.session() as session:
737 | if file_path:
738 | # Use ENDS WITH for flexible path matching
739 | query = """
740 | MATCH (f:Function {name: $function_name})
741 | WHERE f.file_path ENDS WITH $file_path
742 | RETURN f.name as function_name, f.file_path as file_path, f.cyclomatic_complexity as complexity
743 | """
744 | result = session.run(query, function_name=function_name, file_path=file_path)
745 | else:
746 | query = """
747 | MATCH (f:Function {name: $function_name})
748 | RETURN f.name as function_name, f.file_path as file_path, f.cyclomatic_complexity as complexity
749 | """
750 | result = session.run(query, function_name=function_name)
751 |
752 | return [dict(record) for record in result]
753 |
754 | def find_most_complex_functions(self, limit: int = 10) -> List[Dict]:
755 | """Find the most complex functions based on cyclomatic complexity."""
756 | with self.driver.session() as session:
757 | query = """
758 | MATCH (f:Function)
759 | WHERE f.cyclomatic_complexity IS NOT NULL AND f.is_dependency = false
760 | RETURN f.name as function_name, f.file_path as file_path, f.cyclomatic_complexity as complexity, f.line_number as line_number
761 | ORDER BY f.cyclomatic_complexity DESC
762 | LIMIT $limit
763 | """
764 | result = session.run(query, limit=limit)
765 | return [dict(record) for record in result]
766 |
767 | def list_indexed_repositories(self) -> List[Dict]:
768 | """List all indexed repositories."""
769 | with self.driver.session() as session:
770 | result = session.run("""
771 | MATCH (r:Repository)
772 | RETURN r.name as name, r.path as path, r.is_dependency as is_dependency
773 | ORDER BY r.name
774 | """)
775 | return [dict(record) for record in result]
776 |
```
--------------------------------------------------------------------------------
/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 |
```