This project is a high-performance static analysis toolkit for the Ruby language. The goal is to be a solid foundation to power a variety of tools, such as type checkers, linters, language servers and more.
Both Ruby and Rust APIs are made available through a gem and a crate, respectively. Here's a simple example of using the Ruby API:
# Create a new graph representing the current workspace
graph = Rubydex::Graph.new
# Configuring graph LSP encoding
graph.encoding = "utf16"
# Index the entire workspace with all dependencies
graph.index_workspace
# Or index specific file paths
graph.index_all(["path/to/file.rb"])
# Transform the initially collected information into its semantic understanding by running resolution
graph.resolve
# Get all diagnostics acquired during the analysis
graph.diagnostics
# Iterating over graph nodes
graph.declarations
graph.documents
graph.constant_references
graph.method_references
# Analyzing require paths
graph.resolve_require_path("rails/engine", load_paths) # => document pointed by `rails/engine`
graph.require_paths(load_paths) # => array of all indexed require paths
# Querying
graph["Foo"] # Get declaration by fully qualified name
graph.search("Foo#b") # Name search
graph.resolve_constant("Bar", ["Foo", "Baz::Qux"]) # Resolve constant reference based on nesting
# Declarations
declaration = graph["Foo"]
# All declarations include
declaration.name
declaration.unqualified_name
declaration.definitions
declaration.owner
# Namespace declarations include
declaration.member("bar()")
declaration.member("@ivar")
declaration.singleton_class
declaration.ancestors
declaration.descendants
# Documents
document = graph.documents.first
document.uri
document.definitions # => list of definitions discovered in this document
# Definitions
definition = declaration.definitions.first
definition.location
definition.comments
definition.name
definition.deprecated?
definition.name_location
# Locations
location = definition.location
location.path
# Diagnostics
diagnostic = graph.diagnostics.first
diagnostic.rule
diagnostic.message
diagnostic.location
diagnostic.severity
diagnostic.related_informationA resolved Rubydex::Graph can be shared across Ractors without copying:
graph = Rubydex::Graph.new
graph.index_workspace
graph.resolve
Ractor.make_shareable(graph)
# Worker Ractors can now read the graph in parallel
ractor = Ractor.new(graph) { |g| g["Foo"]&.name }
ractor.valueThread safety comes from an RwLock on the Rust side, not from Ruby's
freeze. This is a deliberate, but potentially surprising, choice:
- A frozen (or
make_shareable'd) graph can still be mutated — methods likeindex_source,resolve,exclude_patterns, andencoding=work on a frozen graph. This supports interactive use cases (LSP/MCP) that need incremental edits while worker Ractors read concurrently. - Because the same underlying allocation is shared, callers are responsible for ordering concurrent writes and reads, as with any shared mutable state across Ractors.
- Graphs cannot be
dup'd orclone'd; both raiseRuntimeErrorto avoid aliasing the Rust allocation (which would double-free on GC) or ballooning memory with a deep copy.
All built-in tools are experimental. These tools can change without deprecation warnings.
Rubydex exposes the indexed graph through a read-only subset of the
Cypher query language. Only read clauses (MATCH,
WHERE, RETURN, ...) are supported; there is no way to mutate the graph.
From the command line:
# Run a query against the current workspace
bundle exec rdx query "MATCH (c:Class)-[:DEFINES]->(m:Method) RETURN c.name, m.name"
# Render results as JSON instead of a table
bundle exec rdx query "MATCH (c:Class) RETURN c.name" --format json
# Describe the queryable schema (node labels and relationship types) without indexing
bundle exec rdx query --schemaFrom Ruby:
graph = Rubydex::Graph.new
graph.index_workspace
graph.resolve
# Parse once, render against a graph as a table or JSON string
query = Rubydex::Query.parse("MATCH (c:Class) RETURN c.name")
puts query.render(graph, "table")
puts query.render(graph, "json")
# Describe the schema
puts Rubydex::Query.schema("table")Put rule files under rubydex_linter/rules. Each rule must inherit from Rubydex::Linter::Rule.
Run the linter:
bundle exec rdx lint [PATH]If you omit PATH, Rubydex uses the current directory.
Configure a rule in rubydex.toml:
[linter.rules.<Rule name>]
enabled = true
exclude = ["path_to_skip/**"]Rubydex can run as an MCP (Model Context Protocol) server, enabling AI assistants like Claude to semantically query your Ruby codebase.
-
Add Rubydex to the Ruby project you want to index:
gem "rubydex"
-
Install the bundle:
bundle install
-
Configure your MCP client to run
bundle exec rdx mcp.Using Claude Code as an example:
claude mcp add --scope project rubydex -- bundle exec rdx mcpUsing Codex as an example:
codex mcp add rubydex -- bundle exec rdx mcpStart your MCP client from that project directory. The MCP server indexes the project at startup and provides semantic code intelligence tools through the tools below.
| Tool | Description |
|---|---|
search_declarations |
Fuzzy search for classes, modules, methods, constants |
get_declaration |
Full details by fully qualified name with docs, ancestors, members |
get_descendants |
What classes/modules inherit from or include this one |
find_constant_references |
All precise, resolved constant references across the codebase |
get_file_declarations |
List declarations defined in a specific file |
codebase_stats |
High-level statistics about the indexed codebase |
Rubydex ships with a built in skill library. These skills are referenced by id by Rubydex tools (for example: send-private-method maps to skills/send-private-method/SKILL.md).
The intention is for Rubydex to provide deterministic skill loading for agents. If a rule returns a skill id, the agent can then fetch the skill with rdx skill <id>.
This removes the need for agents to load a large number of possibly unrelated skill descriptions when they may or may not be necessary. The information only surfaces if there is a verified violation to fix, and it only needs to be loaded once, rather than inlining it on every violation in every run of the tool (the common pattern for linters and analyzers).