Developer analyze tool improvement (#5030)

This commit is contained in:
Will Pfleger
2025-10-07 10:20:17 -04:00
committed by GitHub
parent 0600cb4389
commit 4246ca59b3
17 changed files with 1160 additions and 195 deletions
+1
View File
@@ -68,6 +68,7 @@ tree-sitter-go = "0.21"
tree-sitter-java = "0.21"
tree-sitter-kotlin = "0.3.8"
devgen-tree-sitter-swift = "0.21.0"
tree-sitter-ruby = "0.21.0"
streaming-iterator = "0.1"
rayon = "1.10"
libc = "0.2"
@@ -3,6 +3,10 @@ use std::path::PathBuf;
use crate::developer::analyze::types::{AnalysisResult, CallChain};
/// Sentinel value used to represent type references (instantiation, field types, etc.)
/// as callers in the call graph, since they don't have an actual caller function.
const REFERENCE_CALLER: &str = "<reference>";
#[derive(Debug, Clone, Default)]
pub struct CallGraph {
callers: HashMap<String, Vec<(PathBuf, usize, String)>>,
@@ -60,6 +64,44 @@ impl CallGraph {
));
}
}
for reference in &result.references {
use crate::developer::analyze::types::ReferenceType;
match &reference.ref_type {
ReferenceType::MethodDefinition => {
if let Some(type_name) = &reference.associated_type {
tracing::trace!(
"Linking method {} to type {}",
reference.symbol,
type_name
);
graph.callees.entry(type_name.clone()).or_default().push((
file_path.clone(),
reference.line,
reference.symbol.clone(),
));
}
}
ReferenceType::TypeInstantiation
| ReferenceType::FieldType
| ReferenceType::VariableType
| ReferenceType::ParameterType => {
graph
.callers
.entry(reference.symbol.clone())
.or_default()
.push((
file_path.clone(),
reference.line,
REFERENCE_CALLER.to_string(),
));
}
ReferenceType::Definition | ReferenceType::Call | ReferenceType::Import => {
// These are handled elsewhere or not relevant for type tracking
}
}
}
}
tracing::trace!(
@@ -3,17 +3,96 @@ pub const ELEMENT_QUERY: &str = r#"
(function_declaration name: (identifier) @func)
(method_declaration name: (field_identifier) @func)
(type_declaration (type_spec name: (type_identifier) @struct))
(const_declaration (const_spec name: (identifier) @const))
(import_declaration) @import
"#;
/// Tree-sitter query for extracting Go function calls
/// Tree-sitter query for extracting Go function calls and identifier references
pub const CALL_QUERY: &str = r#"
; Function calls
(call_expression
function: (identifier) @function.call)
; Method calls
(call_expression
function: (selector_expression
field: (field_identifier) @method.call))
; Identifier references in various expression contexts
; This captures constants/variables used in arguments, comparisons, returns, assignments, etc.
(argument_list (identifier) @identifier.reference)
(binary_expression left: (identifier) @identifier.reference)
(binary_expression right: (identifier) @identifier.reference)
(unary_expression operand: (identifier) @identifier.reference)
(return_statement (expression_list (identifier) @identifier.reference))
(assignment_statement right: (expression_list (identifier) @identifier.reference))
"#;
/// Tree-sitter query for extracting Go struct references and usage patterns
pub const REFERENCE_QUERY: &str = r#"
; Method receivers - pointer type
(method_declaration
receiver: (parameter_list
(parameter_declaration
type: (pointer_type (type_identifier) @method.receiver))))
; Method receivers - value type
(method_declaration
receiver: (parameter_list
(parameter_declaration
type: (type_identifier) @method.receiver)))
; Struct literals - simple
(composite_literal
type: (type_identifier) @struct.literal)
; Struct literals - qualified (package.Type)
(composite_literal
type: (qualified_type
name: (type_identifier) @struct.literal))
; Field declarations in structs - simple type
(field_declaration
type: (type_identifier) @field.type)
; Field declarations - pointer type
(field_declaration
type: (pointer_type
(type_identifier) @field.type))
; Field declarations - qualified type (package.Type)
(field_declaration
type: (qualified_type
name: (type_identifier) @field.type))
; Field declarations - pointer to qualified type
(field_declaration
type: (pointer_type
(qualified_type
name: (type_identifier) @field.type)))
"#;
/// Find the method name for a method receiver node in Go
///
/// This walks up the tree to find the method_declaration parent and extracts
/// the method name, used for associating methods with their receiver types.
pub fn find_method_for_receiver(
receiver_node: &tree_sitter::Node,
source: &str,
_ast_recursion_limit: Option<usize>,
) -> Option<String> {
let mut current = *receiver_node;
while let Some(parent) = current.parent() {
if parent.kind() == "method_declaration" {
for i in 0..parent.child_count() {
if let Some(child) = parent.child(i) {
if child.kind() == "field_identifier" {
return Some(source[child.byte_range()].to_string());
}
}
}
}
current = parent;
}
None
}
@@ -1,35 +1,154 @@
//! Language-specific analysis implementations
//!
//! This module contains language-specific parsing logic and tree-sitter queries
//! for the analyze tool. Each language has its own submodule with query definitions
//! and optional helper functions.
//!
//! ## Adding a New Language
//!
//! To add support for a new language:
//!
//! 1. Create a new file `languages/yourlang.rs`
//! 2. Define `ELEMENT_QUERY` and `CALL_QUERY` constants
//! 3. Optionally define `REFERENCE_QUERY` for advanced type tracking
//! 4. Add `pub mod yourlang;` below
//! 5. Add language configuration to registry in `get_language_info()`
//!
//! ## Optional Features
//!
//! Languages can opt into additional features by implementing:
//!
//! - Reference tracking: Define `REFERENCE_QUERY` to track type instantiation,
//! field types, and method-to-type associations (see Go and Ruby)
//! - Custom function naming: Implement `extract_function_name_for_kind()` for
//! special cases like Swift's init/deinit or Rust's impl blocks
//! - Method receiver lookup: Implement `find_method_for_receiver()` to associate
//! methods with their containing types (see Go and Ruby)
pub mod go;
pub mod java;
pub mod javascript;
pub mod kotlin;
pub mod python;
pub mod ruby;
pub mod rust;
pub mod swift;
/// Get the tree-sitter query for extracting code elements for a language
pub fn get_element_query(language: &str) -> &'static str {
match language {
"python" => python::ELEMENT_QUERY,
"rust" => rust::ELEMENT_QUERY,
"javascript" | "typescript" => javascript::ELEMENT_QUERY,
"go" => go::ELEMENT_QUERY,
"java" => java::ELEMENT_QUERY,
"kotlin" => kotlin::ELEMENT_QUERY,
"swift" => swift::ELEMENT_QUERY,
_ => "",
}
/// Handler for extracting function names from special node kinds
type ExtractFunctionNameHandler = fn(&tree_sitter::Node, &str, &str) -> Option<String>;
/// Handler for finding method names from receiver nodes
/// Takes: (receiver_node, source, ast_recursion_limit)
type FindMethodForReceiverHandler = fn(&tree_sitter::Node, &str, Option<usize>) -> Option<String>;
/// Language configuration containing all language-specific information
///
/// This struct serves as a single source of truth for language support.
/// All language-specific queries and handlers are defined here.
#[derive(Copy, Clone)]
pub struct LanguageInfo {
/// Tree-sitter query for extracting code elements (functions, classes, imports)
pub element_query: &'static str,
/// Tree-sitter query for extracting function calls
pub call_query: &'static str,
/// Tree-sitter query for extracting type references (optional)
pub reference_query: &'static str,
/// Node kinds that represent function-like constructs
pub function_node_kinds: &'static [&'static str],
/// Node kinds that represent function name identifiers
pub function_name_kinds: &'static [&'static str],
/// Optional handler for language-specific function name extraction
pub extract_function_name_handler: Option<ExtractFunctionNameHandler>,
/// Optional handler for finding method names from receiver nodes
pub find_method_for_receiver_handler: Option<FindMethodForReceiverHandler>,
}
/// Get the tree-sitter query for extracting function calls for a language
pub fn get_call_query(language: &str) -> &'static str {
/// Get language configuration for a given language
///
/// Returns `Some(LanguageInfo)` if the language is supported, `None` otherwise.
pub fn get_language_info(language: &str) -> Option<LanguageInfo> {
match language {
"python" => python::CALL_QUERY,
"rust" => rust::CALL_QUERY,
"javascript" | "typescript" => javascript::CALL_QUERY,
"go" => go::CALL_QUERY,
"java" => java::CALL_QUERY,
"kotlin" => kotlin::CALL_QUERY,
"swift" => swift::CALL_QUERY,
_ => "",
"python" => Some(LanguageInfo {
element_query: python::ELEMENT_QUERY,
call_query: python::CALL_QUERY,
reference_query: "",
function_node_kinds: &["function_definition"],
function_name_kinds: &["identifier", "field_identifier", "property_identifier"],
extract_function_name_handler: None,
find_method_for_receiver_handler: None,
}),
"rust" => Some(LanguageInfo {
element_query: rust::ELEMENT_QUERY,
call_query: rust::CALL_QUERY,
reference_query: "",
function_node_kinds: &["function_item", "impl_item"],
function_name_kinds: &["identifier", "field_identifier", "property_identifier"],
extract_function_name_handler: Some(rust::extract_function_name_for_kind),
find_method_for_receiver_handler: None,
}),
"javascript" | "typescript" => Some(LanguageInfo {
element_query: javascript::ELEMENT_QUERY,
call_query: javascript::CALL_QUERY,
reference_query: "",
function_node_kinds: &[
"function_declaration",
"method_definition",
"arrow_function",
],
function_name_kinds: &["identifier", "field_identifier", "property_identifier"],
extract_function_name_handler: None,
find_method_for_receiver_handler: None,
}),
"go" => Some(LanguageInfo {
element_query: go::ELEMENT_QUERY,
call_query: go::CALL_QUERY,
reference_query: go::REFERENCE_QUERY,
function_node_kinds: &["function_declaration", "method_declaration"],
function_name_kinds: &["identifier", "field_identifier", "property_identifier"],
extract_function_name_handler: None,
find_method_for_receiver_handler: Some(go::find_method_for_receiver),
}),
"java" => Some(LanguageInfo {
element_query: java::ELEMENT_QUERY,
call_query: java::CALL_QUERY,
reference_query: "",
function_node_kinds: &["method_declaration", "constructor_declaration"],
function_name_kinds: &["identifier", "field_identifier", "property_identifier"],
extract_function_name_handler: None,
find_method_for_receiver_handler: None,
}),
"kotlin" => Some(LanguageInfo {
element_query: kotlin::ELEMENT_QUERY,
call_query: kotlin::CALL_QUERY,
reference_query: "",
function_node_kinds: &["function_declaration", "class_body"],
function_name_kinds: &["identifier", "field_identifier", "property_identifier"],
extract_function_name_handler: None,
find_method_for_receiver_handler: None,
}),
"swift" => Some(LanguageInfo {
element_query: swift::ELEMENT_QUERY,
call_query: swift::CALL_QUERY,
reference_query: "",
function_node_kinds: &[
"function_declaration",
"init_declaration",
"deinit_declaration",
"subscript_declaration",
],
function_name_kinds: &["simple_identifier"],
extract_function_name_handler: Some(swift::extract_function_name_for_kind),
find_method_for_receiver_handler: None,
}),
"ruby" => Some(LanguageInfo {
element_query: ruby::ELEMENT_QUERY,
call_query: ruby::CALL_QUERY,
reference_query: ruby::REFERENCE_QUERY,
function_node_kinds: &["method", "singleton_method"],
function_name_kinds: &["identifier", "field_identifier", "property_identifier"],
extract_function_name_handler: None,
find_method_for_receiver_handler: Some(ruby::find_method_for_receiver),
}),
_ => None,
}
}
@@ -0,0 +1,151 @@
/// Tree-sitter query for extracting Ruby code elements.
///
/// This query captures:
/// - Method definitions (def)
/// - Class and module definitions
/// - Constants
/// - Common attr_* declarations (attr_accessor, attr_reader, attr_writer)
/// - Import statements (require, require_relative, load)
pub const ELEMENT_QUERY: &str = r#"
; Method definitions
(method name: (identifier) @func)
; Class and module definitions
(class name: (constant) @class)
(module name: (constant) @class)
; Constant assignments
(assignment left: (constant) @const)
; Attr declarations as functions
(call method: (identifier) @func (#eq? @func "attr_accessor"))
(call method: (identifier) @func (#eq? @func "attr_reader"))
(call method: (identifier) @func (#eq? @func "attr_writer"))
; Require statements
(call method: (identifier) @import (#eq? @import "require"))
(call method: (identifier) @import (#eq? @import "require_relative"))
(call method: (identifier) @import (#eq? @import "load"))
"#;
/// Tree-sitter query for extracting Ruby function calls.
///
/// This query captures:
/// - Direct method calls
/// - Method calls with receivers (object.method)
/// - Calls to constants (typically constructors like ClassName.new)
/// - Identifier and constant references in various expression contexts
pub const CALL_QUERY: &str = r#"
; Method calls
(call method: (identifier) @method.call)
; Method calls with receiver
(call receiver: (_) method: (identifier) @method.call)
; Calls to constants (typically constructors)
(call receiver: (constant) @function.call)
; Identifier and constant references in argument lists
(argument_list (identifier) @identifier.reference)
(argument_list (constant) @identifier.reference)
; Binary expressions
(binary left: (identifier) @identifier.reference)
(binary right: (identifier) @identifier.reference)
(binary left: (constant) @identifier.reference)
(binary right: (constant) @identifier.reference)
; Assignment expressions
(assignment right: (identifier) @identifier.reference)
(assignment right: (constant) @identifier.reference)
"#;
/// Tree-sitter query for extracting Ruby type references and usage patterns.
///
/// This query captures:
/// - Method-to-class associations (instance and class methods)
/// - Class instantiation (ClassName.new)
/// - Type references in various contexts
pub const REFERENCE_QUERY: &str = r#"
; Instance methods within a class - capture class name, will find method via receiver lookup
(class
name: (constant) @method.receiver
(body_statement (method)))
; Class instantiation (ClassName.new)
(call
receiver: (constant) @struct.literal
method: (identifier) @method.name (#eq? @method.name "new"))
; Constant references as receivers (type usage)
(call
receiver: (constant) @field.type
method: (identifier))
"#;
/// Find the method name for a method receiver node in Ruby
///
/// For Ruby, the receiver_node is the class constant. This finds methods
/// within that class node, used for associating methods with their classes.
pub fn find_method_for_receiver(
receiver_node: &tree_sitter::Node,
source: &str,
ast_recursion_limit: Option<usize>,
) -> Option<String> {
let max_depth = ast_recursion_limit.unwrap_or(10);
// For Ruby, receiver_node is the class constant
if receiver_node.kind() == "constant" {
let mut current = *receiver_node;
while let Some(parent) = current.parent() {
if parent.kind() == "class" {
return find_first_method_in_class(&parent, source, max_depth);
}
current = parent;
}
}
None
}
/// Find the first method name within a Ruby class node
fn find_first_method_in_class(
class_node: &tree_sitter::Node,
source: &str,
max_depth: usize,
) -> Option<String> {
for i in 0..class_node.child_count() {
if let Some(child) = class_node.child(i) {
if child.kind() == "body_statement" {
return find_method_in_body_with_depth(&child, source, 0, max_depth);
}
}
}
None
}
/// Recursively find a method within a body_statement node with depth limit
fn find_method_in_body_with_depth(
node: &tree_sitter::Node,
source: &str,
depth: usize,
max_depth: usize,
) -> Option<String> {
if depth >= max_depth {
return None;
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if child.kind() == "method" {
for j in 0..child.child_count() {
if let Some(name_node) = child.child(j) {
if name_node.kind() == "identifier" {
return Some(source[name_node.byte_range()].to_string());
}
}
}
}
}
}
None
}
@@ -26,3 +26,25 @@ pub const CALL_QUERY: &str = r#"
(macro_invocation
macro: (identifier) @macro.call)
"#;
/// Extract function name for Rust-specific node kinds
///
/// Rust has special cases like impl_item blocks that should be
/// formatted as "impl TypeName" instead of extracting a simple name.
pub fn extract_function_name_for_kind(
node: &tree_sitter::Node,
source: &str,
kind: &str,
) -> Option<String> {
if kind == "impl_item" {
// For impl blocks, find the type being implemented
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if child.kind() == "type_identifier" {
return Some(format!("impl {}", &source[child.byte_range()]));
}
}
}
}
None
}
@@ -54,3 +54,19 @@ pub const CALL_QUERY: &str = r#"
(call_expression
(navigation_expression) @function.call)
"#;
/// Extract function name for Swift-specific node kinds
///
/// Swift has special cases like init_declaration and deinit_declaration
/// that should return fixed names instead of extracting from children.
pub fn extract_function_name_for_kind(
_node: &tree_sitter::Node,
_source: &str,
kind: &str,
) -> Option<String> {
match kind {
"init_declaration" => Some("init".to_string()),
"deinit_declaration" => Some("deinit".to_string()),
_ => None,
}
}
+22 -41
View File
@@ -53,7 +53,6 @@ impl Default for CodeAnalyzer {
}
impl CodeAnalyzer {
/// Create a new code analyzer
pub fn new() -> Self {
tracing::debug!("Initializing CodeAnalyzer");
Self {
@@ -62,7 +61,6 @@ impl CodeAnalyzer {
}
}
/// Main analyze entry point
pub fn analyze(
&self,
params: AnalyzeParams,
@@ -83,16 +81,15 @@ impl CodeAnalyzer {
AnalysisMode::Focused => self.analyze_focused(&path, &params, &traverser)?,
AnalysisMode::Semantic => {
if path.is_file() {
let result = self.analyze_file(&path, &mode)?;
let result = self.analyze_file(&path, &mode, &params)?;
Formatter::format_analysis_result(&path, &result, &mode)
} else {
// Semantic mode on directory - analyze all files
self.analyze_directory(&path, &params, &traverser, &mode)?
}
}
AnalysisMode::Structure => {
if path.is_file() {
let result = self.analyze_file(&path, &mode)?;
let result = self.analyze_file(&path, &mode, &params)?;
Formatter::format_analysis_result(&path, &result, &mode)
} else {
self.analyze_directory(&path, &params, &traverser, &mode)?
@@ -107,7 +104,6 @@ impl CodeAnalyzer {
}
}
// Check output size and warn if too large (unless force flag is set)
const OUTPUT_LIMIT: usize = 1000;
if !params.force {
let line_count = output.lines().count();
@@ -142,14 +138,11 @@ impl CodeAnalyzer {
Ok(CallToolResult::success(Formatter::format_results(output)))
}
/// Determine the analysis mode based on parameters and path
fn determine_mode(&self, params: &AnalyzeParams, path: &Path) -> AnalysisMode {
// If focus is specified, use focused mode
if params.focus.is_some() {
return AnalysisMode::Focused;
}
// Otherwise, use semantic for files, structure for directories
if path.is_file() {
AnalysisMode::Semantic
} else {
@@ -157,11 +150,14 @@ impl CodeAnalyzer {
}
}
/// Analyze a single file
fn analyze_file(&self, path: &Path, mode: &AnalysisMode) -> Result<AnalysisResult, ErrorData> {
fn analyze_file(
&self,
path: &Path,
mode: &AnalysisMode,
params: &AnalyzeParams,
) -> Result<AnalysisResult, ErrorData> {
tracing::debug!("Analyzing file {:?} in {:?} mode", path, mode);
// Check cache first
let metadata = std::fs::metadata(path).map_err(|e| {
tracing::error!("Failed to get file metadata for {:?}: {}", path, e);
ErrorData::new(
@@ -183,61 +179,56 @@ impl CodeAnalyzer {
)
})?;
// Check cache
if let Some(cached) = self.cache.get(&path.to_path_buf(), modified) {
tracing::trace!("Using cached result for {:?}", path);
return Ok(cached);
}
// Read file content - handle binary files gracefully
let content = match std::fs::read_to_string(path) {
Ok(content) => content,
Err(e) => {
// Binary or non-UTF-8 file, skip parsing
tracing::trace!("Skipping binary/non-UTF-8 file {:?}: {}", path, e);
return Ok(AnalysisResult::empty(0));
}
};
// Count lines
let line_count = content.lines().count();
// Get language
let language = lang::get_language_identifier(path);
if language.is_empty() {
tracing::trace!("Unsupported file type: {:?}", path);
// Unsupported language, return empty result
return Ok(AnalysisResult::empty(line_count));
}
// Check if we support this language for parsing
let supported = matches!(
language,
"python" | "rust" | "javascript" | "typescript" | "go" | "java" | "kotlin" | "swift"
);
// A language is supported if it has query definitions
let language_supported = languages::get_language_info(language)
.map(|info| !info.element_query.is_empty())
.unwrap_or(false);
if !supported {
if !language_supported {
tracing::trace!("Language {} not supported for parsing", language);
return Ok(AnalysisResult::empty(line_count));
}
// Parse the file
let tree = self.parser_manager.parse(&content, language)?;
// Extract information based on mode
let depth = mode.as_str();
let mut result = ElementExtractor::extract_with_depth(&tree, &content, language, depth)?;
let mut result = ElementExtractor::extract_with_depth(
&tree,
&content,
language,
depth,
params.ast_recursion_limit,
)?;
// Add line count to the result
result.line_count = line_count;
// Cache the result
self.cache.put(path.to_path_buf(), modified, result.clone());
Ok(result)
}
/// Analyze a directory
fn analyze_directory(
&self,
path: &Path,
@@ -249,12 +240,10 @@ impl CodeAnalyzer {
let mode = *mode;
// Collect directory results with parallel processing
let results = traverser.collect_directory_results(path, params.max_depth, |file_path| {
self.analyze_file(file_path, &mode)
self.analyze_file(file_path, &mode, params)
})?;
// Format based on mode
Ok(Formatter::format_directory_structure(
path,
&results,
@@ -262,14 +251,12 @@ impl CodeAnalyzer {
))
}
/// Focused mode analysis - track a symbol across files
fn analyze_focused(
&self,
path: &Path,
params: &AnalyzeParams,
traverser: &FileTraverser<'_>,
) -> Result<String, ErrorData> {
// Focused mode requires focus parameter
let focus_symbol = params.focus.as_ref().ok_or_else(|| {
ErrorData::new(
ErrorCode::INVALID_PARAMS,
@@ -281,7 +268,6 @@ impl CodeAnalyzer {
tracing::info!("Running focused analysis for symbol '{}'", focus_symbol);
// Step 1: Collect all files to analyze
let files_to_analyze = if path.is_file() {
vec![path.to_path_buf()]
} else {
@@ -293,21 +279,18 @@ impl CodeAnalyzer {
files_to_analyze.len()
);
// Step 2: Analyze all files and collect results using parallel processing
use rayon::prelude::*;
let all_results: Result<Vec<_>, _> = files_to_analyze
.par_iter()
.map(|file_path| {
self.analyze_file(file_path, &AnalysisMode::Semantic)
self.analyze_file(file_path, &AnalysisMode::Semantic, params)
.map(|result| (file_path.clone(), result))
})
.collect();
let all_results = all_results?;
// Step 3: Build the call graph
let graph = CallGraph::build_from_results(&all_results);
// Step 4: Find call chains based on follow_depth
let incoming_chains = if params.follow_depth > 0 {
graph.find_incoming_chains(focus_symbol, params.follow_depth)
} else {
@@ -320,14 +303,12 @@ impl CodeAnalyzer {
vec![]
};
// Step 5: Get definitions from graph
let definitions = graph
.definitions
.get(focus_symbol)
.cloned()
.unwrap_or_default();
// Step 6: Format the output
let focus_data = FocusedAnalysisData {
focus_symbol,
follow_depth: params.follow_depth,
+183 -120
View File
@@ -9,7 +9,6 @@ use crate::developer::analyze::types::{
ReferenceType,
};
/// Manages tree-sitter parsers for different languages
#[derive(Clone)]
pub struct ParserManager {
parsers: Arc<Mutex<HashMap<String, Arc<Mutex<Parser>>>>>,
@@ -23,7 +22,6 @@ impl ParserManager {
}
}
/// Get or create a parser for the specified language
pub fn get_or_create_parser(&self, language: &str) -> Result<Arc<Mutex<Parser>>, ErrorData> {
let mut cache = lock_or_recover(&self.parsers, |c| c.clear());
@@ -42,6 +40,7 @@ impl ParserManager {
"java" => tree_sitter_java::language(),
"kotlin" => tree_sitter_kotlin::language(),
"swift" => devgen_tree_sitter_swift::language(),
"ruby" => tree_sitter_ruby::language(),
_ => {
tracing::warn!("Unsupported language: {}", language);
return Err(ErrorData::new(
@@ -66,10 +65,8 @@ impl ParserManager {
Ok(parser_arc)
}
/// Parse source code and return the syntax tree
pub fn parse(&self, content: &str, language: &str) -> Result<Tree, ErrorData> {
let parser_arc = self.get_or_create_parser(language)?;
// Parser doesn't have a clear() method, so we just continue with it
let mut parser = lock_or_recover(&parser_arc, |_| {});
parser.parse(content, None).ok_or_else(|| {
@@ -89,66 +86,94 @@ impl Default for ParserManager {
}
}
/// Extract code elements from a parsed tree
pub struct ElementExtractor;
impl ElementExtractor {
/// Extract code elements with optional semantic analysis
fn find_child_by_kind<'a>(
node: &'a tree_sitter::Node,
kinds: &[&str],
) -> Option<tree_sitter::Node<'a>> {
(0..node.child_count())
.filter_map(|i| node.child(i))
.find(|child| kinds.contains(&child.kind()))
}
fn extract_text_from_child(
node: &tree_sitter::Node,
source: &str,
kinds: &[&str],
) -> Option<String> {
Self::find_child_by_kind(node, kinds).map(|child| source[child.byte_range()].to_string())
}
pub fn extract_with_depth(
tree: &Tree,
source: &str,
language: &str,
depth: &str,
ast_recursion_limit: Option<usize>,
) -> Result<AnalysisResult, ErrorData> {
use crate::developer::analyze::languages;
tracing::trace!(
"Extracting elements from {} code with depth {}",
language,
depth
);
// First get the structural analysis
let mut result = Self::extract_elements(tree, source, language)?;
// For structure mode, clear the detailed vectors but keep the counts
if depth == "structure" {
result.functions.clear();
result.classes.clear();
result.imports.clear();
} else if depth == "semantic" {
// For semantic mode, also extract calls
let calls = Self::extract_calls(tree, source, language)?;
result.calls = calls;
// Also populate references from the calls
for call in &result.calls {
result.references.push(ReferenceInfo {
symbol: call.callee_name.clone(),
ref_type: ReferenceType::Call,
line: call.line,
context: call.context.clone(),
associated_type: None,
});
}
// Languages can opt-in to advanced reference tracking by providing a REFERENCE_QUERY
// in their language definition. This enables tracking of:
// - Type instantiation (struct literals, object creation)
// - Field/variable/parameter type references
// - Method-to-type associations
if let Some(info) = languages::get_language_info(language) {
if !info.reference_query.is_empty() {
let references =
Self::extract_references(tree, source, language, ast_recursion_limit)?;
result.references.extend(references);
}
}
}
Ok(result)
}
/// Extract basic code elements (functions, classes, imports)
pub fn extract_elements(
tree: &Tree,
source: &str,
language: &str,
) -> Result<AnalysisResult, ErrorData> {
// Get language-specific query
let query_str = Self::get_element_query(language);
if query_str.is_empty() {
return Ok(Self::empty_analysis_result());
}
use crate::developer::analyze::languages;
let info = match languages::get_language_info(language) {
Some(info) if !info.element_query.is_empty() => info,
_ => return Ok(Self::empty_analysis_result()),
};
let query_str = info.element_query;
// Parse and process the query
let (functions, classes, imports) = Self::process_element_query(tree, source, query_str)?;
// Detect main function
let main_line = functions.iter().find(|f| f.name == "main").map(|f| f.line);
Ok(AnalysisResult {
@@ -165,23 +190,6 @@ impl ElementExtractor {
})
}
/// Get language-specific query for elements
fn get_element_query(language: &str) -> &'static str {
use crate::developer::analyze::languages;
match language {
"python" => languages::python::ELEMENT_QUERY,
"rust" => languages::rust::ELEMENT_QUERY,
"javascript" | "typescript" => languages::javascript::ELEMENT_QUERY,
"go" => languages::go::ELEMENT_QUERY,
"java" => languages::java::ELEMENT_QUERY,
"kotlin" => languages::kotlin::ELEMENT_QUERY,
"swift" => languages::swift::ELEMENT_QUERY,
_ => "",
}
}
/// Process element query and extract functions, classes, imports
fn process_element_query(
tree: &Tree,
source: &str,
@@ -212,7 +220,7 @@ impl ElementExtractor {
let line = source[..node.start_byte()].lines().count() + 1;
match query.capture_names()[capture.index as usize] {
"func" => {
"func" | "const" => {
functions.push(FunctionInfo {
name: text.to_string(),
line,
@@ -244,37 +252,22 @@ impl ElementExtractor {
Ok((functions, classes, imports))
}
/// Get language-specific query for finding function calls
fn get_call_query(language: &str) -> &'static str {
use crate::developer::analyze::languages;
match language {
"python" => languages::python::CALL_QUERY,
"rust" => languages::rust::CALL_QUERY,
"javascript" | "typescript" => languages::javascript::CALL_QUERY,
"go" => languages::go::CALL_QUERY,
"java" => languages::java::CALL_QUERY,
"kotlin" => languages::kotlin::CALL_QUERY,
"swift" => languages::swift::CALL_QUERY,
_ => "",
}
}
/// Extract function calls from the parse tree
fn extract_calls(
tree: &Tree,
source: &str,
language: &str,
) -> Result<Vec<CallInfo>, ErrorData> {
use crate::developer::analyze::languages;
use tree_sitter::{Query, QueryCursor};
let mut calls = Vec::new();
// Get language-specific call query
let query_str = Self::get_call_query(language);
if query_str.is_empty() {
return Ok(calls); // No call query for this language
}
let info = match languages::get_language_info(language) {
Some(info) if !info.call_query.is_empty() => info,
_ => return Ok(calls),
};
let query_str = info.call_query;
let query = Query::new(&tree.language(), query_str).map_err(|e| {
tracing::error!("Failed to create call query: {}", e);
@@ -294,7 +287,6 @@ impl ElementExtractor {
let text = &source[node.byte_range()];
let start_pos = node.start_position();
// Get the line of code for context
let line_start = source[..node.start_byte()]
.rfind('\n')
.map(|i| i + 1)
@@ -305,13 +297,15 @@ impl ElementExtractor {
.unwrap_or(source.len());
let context = source[line_start..line_end].trim().to_string();
// Find the containing function
let caller_name = Self::find_containing_function(&node, source, language);
// Add the call based on capture name
match query.capture_names()[capture.index as usize] {
"function.call" | "method.call" | "scoped.call" | "macro.call"
| "constructor.call" => {
"function.call"
| "method.call"
| "scoped.call"
| "macro.call"
| "constructor.call"
| "identifier.reference" => {
calls.push(CallInfo {
caller_name,
callee_name: text.to_string(),
@@ -329,82 +323,151 @@ impl ElementExtractor {
Ok(calls)
}
/// Find which function contains a given node
fn extract_references(
tree: &Tree,
source: &str,
language: &str,
ast_recursion_limit: Option<usize>,
) -> Result<Vec<ReferenceInfo>, ErrorData> {
use crate::developer::analyze::languages;
use tree_sitter::{Query, QueryCursor};
let mut references = Vec::new();
let info = match languages::get_language_info(language) {
Some(info) if !info.reference_query.is_empty() => info,
_ => return Ok(references),
};
let query_str = info.reference_query;
let query = Query::new(&tree.language(), query_str).map_err(|e| {
tracing::error!("Failed to create reference query: {}", e);
ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to create reference query: {}", e),
None,
)
})?;
let mut cursor = QueryCursor::new();
let mut matches = cursor.matches(&query, tree.root_node(), source.as_bytes());
for match_ in matches.by_ref() {
for capture in match_.captures {
let node = capture.node;
let text = &source[node.byte_range()];
let start_pos = node.start_position();
let line_start = source[..node.start_byte()]
.rfind('\n')
.map(|i| i + 1)
.unwrap_or(0);
let line_end = source[node.end_byte()..]
.find('\n')
.map(|i| node.end_byte() + i)
.unwrap_or(source.len());
let context = source[line_start..line_end].trim().to_string();
let capture_name = query.capture_names()[capture.index as usize];
let (ref_type, symbol, associated_type) = match capture_name {
"method.receiver" => {
let method_name = Self::find_method_name_for_receiver(
&node,
source,
language,
ast_recursion_limit,
);
if let Some(method_name) = method_name {
(
ReferenceType::MethodDefinition,
method_name,
Some(text.to_string()),
)
} else {
continue;
}
}
"struct.literal" => (ReferenceType::TypeInstantiation, text.to_string(), None),
"field.type" => (ReferenceType::FieldType, text.to_string(), None),
"param.type" => (ReferenceType::ParameterType, text.to_string(), None),
"var.type" | "shortvar.type" => {
(ReferenceType::VariableType, text.to_string(), None)
}
"type.assertion" | "type.conversion" => {
(ReferenceType::Call, text.to_string(), None)
}
_ => continue,
};
references.push(ReferenceInfo {
symbol,
ref_type,
line: start_pos.row + 1,
context,
associated_type,
});
}
}
tracing::trace!("Extracted {} struct references", references.len());
Ok(references)
}
fn find_method_name_for_receiver(
receiver_node: &tree_sitter::Node,
source: &str,
language: &str,
ast_recursion_limit: Option<usize>,
) -> Option<String> {
use crate::developer::analyze::languages;
languages::get_language_info(language)
.and_then(|info| info.find_method_for_receiver_handler)
.and_then(|handler| handler(receiver_node, source, ast_recursion_limit))
}
fn find_containing_function(
node: &tree_sitter::Node,
source: &str,
language: &str,
) -> Option<String> {
use crate::developer::analyze::languages;
let info = languages::get_language_info(language)?;
let mut current = *node;
// Walk up the tree to find a function definition
while let Some(parent) = current.parent() {
let kind = parent.kind();
// Check for function-like nodes based on language
let is_function = match language {
"python" => kind == "function_definition",
"rust" => kind == "function_item" || kind == "impl_item",
"javascript" | "typescript" => {
kind == "function_declaration"
|| kind == "method_definition"
|| kind == "arrow_function"
}
"go" => kind == "function_declaration" || kind == "method_declaration",
"java" => kind == "method_declaration" || kind == "constructor_declaration",
"kotlin" => kind == "function_declaration" || kind == "class_body",
"swift" => {
kind == "function_declaration"
|| kind == "init_declaration"
|| kind == "deinit_declaration"
|| kind == "subscript_declaration"
}
_ => false,
};
if is_function {
// Try to extract the function name
for i in 0..parent.child_count() {
if let Some(child) = parent.child(i) {
// Look for identifier nodes that represent the function name
if child.kind() == "identifier"
|| child.kind() == "field_identifier"
|| child.kind() == "property_identifier"
|| (language == "swift" && child.kind() == "simple_identifier")
{
// For Python, skip the first identifier if it's 'def'
if language == "python" && i == 0 {
continue;
}
// For Swift init/deinit, use special names
if language == "swift" {
if kind == "init_declaration" {
return Some("init".to_string());
} else if kind == "deinit_declaration" {
return Some("deinit".to_string());
}
}
return Some(source[child.byte_range()].to_string());
}
// For Rust impl blocks, look for the type
if language == "rust"
&& kind == "impl_item"
&& child.kind() == "type_identifier"
{
return Some(format!("impl {}", &source[child.byte_range()]));
}
// Check if this is a function-like node
if info.function_node_kinds.contains(&kind) {
// Two-step extraction process:
// 1. Try language-specific extraction for special cases (e.g., Rust impl blocks, Swift init/deinit)
// 2. Fall back to generic extraction using standard identifier node kinds
// This pattern allows languages to override default behavior when needed
if let Some(handler) = info.extract_function_name_handler {
if let Some(name) = handler(&parent, source, kind) {
return Some(name);
}
}
// Standard extraction: find first child matching expected identifier kinds
if let Some(name) =
Self::extract_text_from_child(&parent, source, info.function_name_kinds)
{
return Some(name);
}
}
current = parent;
}
None // No containing function found (module-level call)
None
}
/// Create an empty analysis result
fn empty_analysis_result() -> AnalysisResult {
AnalysisResult {
functions: vec![],
@@ -0,0 +1,115 @@
use crate::developer::analyze::graph::CallGraph;
use crate::developer::analyze::parser::{ElementExtractor, ParserManager};
use crate::developer::analyze::types::{AnalysisResult, ReferenceType};
use std::collections::HashSet;
use std::path::PathBuf;
fn parse_and_extract(code: &str) -> AnalysisResult {
let manager = ParserManager::new();
let tree = manager.parse(code, "go").unwrap();
ElementExtractor::extract_with_depth(&tree, code, "go", "semantic", None).unwrap()
}
fn build_test_graph(files: Vec<(&str, &str)>) -> CallGraph {
let manager = ParserManager::new();
let results: Vec<_> = files
.iter()
.map(|(path, code)| {
let tree = manager.parse(code, "go").unwrap();
let result =
ElementExtractor::extract_with_depth(&tree, code, "go", "semantic", None).unwrap();
(PathBuf::from(*path), result)
})
.collect();
CallGraph::build_from_results(&results)
}
#[test]
fn test_go_struct_and_method_tracking() {
let code = r#"
package main
import "myapp/pkg/service"
type Config struct {
Host string
Port int
}
type Handler struct {
Cfg *Config
Svc *service.Widget
}
func (h *Handler) Start() error {
return nil
}
func (h *Handler) Stop() error {
return nil
}
func main() {
cfg := Config{Host: "localhost", Port: 8080}
handler := Handler{Cfg: &cfg}
_ = handler.Start()
}
"#;
let result = parse_and_extract(code);
let graph = build_test_graph(vec![("test.go", code)]);
assert_eq!(result.class_count, 2);
let struct_names: HashSet<_> = result.classes.iter().map(|c| c.name.as_str()).collect();
assert!(struct_names.contains("Config"));
assert!(struct_names.contains("Handler"));
assert_eq!(result.function_count, 3);
let method_names: HashSet<_> = result.functions.iter().map(|f| f.name.as_str()).collect();
assert!(method_names.contains("Start"));
assert!(method_names.contains("Stop"));
assert!(method_names.contains("main"));
let handler_methods: Vec<_> = result
.references
.iter()
.filter(|r| {
r.ref_type == ReferenceType::MethodDefinition
&& r.associated_type.as_deref() == Some("Handler")
})
.collect();
assert!(
handler_methods.len() >= 2,
"Expected at least 2 methods on Handler, found {}",
handler_methods.len()
);
let field_type_refs: Vec<_> = result
.references
.iter()
.filter(|r| r.ref_type == ReferenceType::FieldType)
.collect();
assert!(
!field_type_refs.is_empty(),
"Expected to find field type references"
);
let config_literals: Vec<_> = result
.references
.iter()
.filter(|r| r.symbol == "Config" && r.ref_type == ReferenceType::TypeInstantiation)
.collect();
assert!(
!config_literals.is_empty(),
"Expected to find Config struct literals"
);
let incoming = graph.find_incoming_chains("Handler", 1);
assert!(
!incoming.is_empty(),
"Expected to find incoming references to Handler"
);
let outgoing = graph.find_outgoing_chains("Handler", 1);
assert!(!outgoing.is_empty(), "Expected to find methods on Handler");
}
@@ -17,6 +17,7 @@ fn test_analyze_python_file() {
focus: None,
follow_depth: 2,
max_depth: 3,
ast_recursion_limit: None,
force: false,
};
@@ -45,6 +46,7 @@ fn test_analyze_directory() {
focus: None,
follow_depth: 2,
max_depth: 3,
ast_recursion_limit: None,
force: false,
};
@@ -81,6 +83,7 @@ fn test_focused_analysis() {
focus: Some("helper".to_string()),
follow_depth: 1,
max_depth: 3,
ast_recursion_limit: None,
force: false,
};
@@ -109,6 +112,7 @@ fn test_analyze_with_cache() {
focus: None,
follow_depth: 2,
max_depth: 3,
ast_recursion_limit: None,
force: false,
};
@@ -140,6 +144,7 @@ fn test_analyze_unsupported_file() {
focus: None,
follow_depth: 2,
max_depth: 3,
ast_recursion_limit: None,
force: false,
};
@@ -158,6 +163,7 @@ fn test_analyze_nonexistent_path() {
focus: None,
follow_depth: 2,
max_depth: 3,
ast_recursion_limit: None,
force: false,
};
@@ -182,6 +188,7 @@ fn test_focused_without_symbol() {
focus: Some("nonexistent_symbol".to_string()),
follow_depth: 1,
max_depth: 3,
ast_recursion_limit: None,
force: false,
};
@@ -219,6 +226,7 @@ fn test_nested_directory_analysis() {
focus: None,
follow_depth: 2,
max_depth: 3, // Increase max_depth to ensure we reach nested files
ast_recursion_limit: None,
force: false,
};
@@ -34,6 +34,7 @@ fn test_large_output_warning() {
focus: None,
follow_depth: 2,
max_depth: 3,
ast_recursion_limit: None,
force: false, // Should trigger warning
};
@@ -82,6 +83,7 @@ fn test_force_flag_bypasses_warning() {
focus: None,
follow_depth: 2,
max_depth: 3,
ast_recursion_limit: None,
force: true, // Should bypass warning
};
@@ -119,6 +121,7 @@ fn test_small_output_no_warning() {
focus: None,
follow_depth: 2,
max_depth: 3,
ast_recursion_limit: None,
force: false, // Shouldn't matter for small output
};
@@ -3,8 +3,10 @@
pub mod cache_tests;
pub mod fixtures;
pub mod formatter_tests;
pub mod go_test;
pub mod graph_tests;
pub mod integration_tests;
pub mod large_output_tests;
pub mod parser_tests;
pub mod ruby_test;
pub mod traversal_tests;
@@ -118,7 +118,7 @@ def func2():
let tree = manager.parse(content, "python").unwrap();
let result =
ElementExtractor::extract_with_depth(&tree, content, "python", "structure").unwrap();
ElementExtractor::extract_with_depth(&tree, content, "python", "structure", None).unwrap();
// In structure mode, detailed vectors should be empty but counts preserved
assert_eq!(result.function_count, 2);
@@ -139,7 +139,7 @@ def func2():
let tree = manager.parse(content, "python").unwrap();
let result =
ElementExtractor::extract_with_depth(&tree, content, "python", "semantic").unwrap();
ElementExtractor::extract_with_depth(&tree, content, "python", "semantic", None).unwrap();
// In semantic mode, should have both elements and calls
assert_eq!(result.function_count, 2);
@@ -226,3 +226,80 @@ fun helper() {
assert!(result.import_count > 0); // import statements
assert!(result.main_line.is_some());
}
#[test]
fn test_language_registry() {
use crate::developer::analyze::languages;
let supported = vec![
"python",
"rust",
"javascript",
"typescript",
"go",
"java",
"kotlin",
"swift",
"ruby",
];
for lang in supported {
let info = languages::get_language_info(lang);
assert!(info.is_some(), "Language {} should be supported", lang);
let info = info.unwrap();
assert!(
!info.element_query.is_empty(),
"{} missing element_query",
lang
);
assert!(!info.call_query.is_empty(), "{} missing call_query", lang);
assert!(
!info.function_node_kinds.is_empty(),
"{} missing function_node_kinds",
lang
);
assert!(
!info.function_name_kinds.is_empty(),
"{} missing function_name_kinds",
lang
);
}
let js = languages::get_language_info("javascript").unwrap();
let ts = languages::get_language_info("typescript").unwrap();
assert_eq!(
js.element_query, ts.element_query,
"JS/TS should share config"
);
let go = languages::get_language_info("go").unwrap();
assert!(
!go.reference_query.is_empty(),
"Go should have reference tracking"
);
assert!(go.find_method_for_receiver_handler.is_some());
let ruby = languages::get_language_info("ruby").unwrap();
assert!(
!ruby.reference_query.is_empty(),
"Ruby should have reference tracking"
);
assert!(ruby.find_method_for_receiver_handler.is_some());
let rust = languages::get_language_info("rust").unwrap();
assert!(
rust.extract_function_name_handler.is_some(),
"Rust should have custom handler"
);
let swift = languages::get_language_info("swift").unwrap();
assert!(
swift.extract_function_name_handler.is_some(),
"Swift should have custom handler"
);
assert!(languages::get_language_info("unsupported").is_none());
assert!(languages::get_language_info("").is_none());
assert!(languages::get_language_info("C++").is_none());
}
@@ -0,0 +1,259 @@
#[cfg(test)]
mod ruby_tests {
use crate::developer::analyze::graph::CallGraph;
use crate::developer::analyze::parser::{ElementExtractor, ParserManager};
use crate::developer::analyze::types::ReferenceType;
use std::collections::HashSet;
use std::path::PathBuf;
#[test]
fn test_ruby_basic_parsing() {
let parser = ParserManager::new();
let source = r#"
require 'json'
class MyClass
attr_accessor :name
def initialize(name)
@name = name
end
def greet
puts "Hello"
end
end
"#;
let tree = parser.parse(source, "ruby").unwrap();
let result = ElementExtractor::extract_elements(&tree, source, "ruby").unwrap();
assert_eq!(result.class_count, 1);
assert!(result.classes.iter().any(|c| c.name == "MyClass"));
assert!(result.function_count > 0);
assert!(result.functions.iter().any(|f| f.name == "initialize"));
assert!(result.functions.iter().any(|f| f.name == "greet"));
assert!(result.import_count > 0);
}
#[test]
fn test_ruby_attr_methods() {
let parser = ParserManager::new();
let source = r#"
class Person
attr_reader :age
attr_writer :status
attr_accessor :name
end
"#;
let tree = parser.parse(source, "ruby").unwrap();
let result = ElementExtractor::extract_elements(&tree, source, "ruby").unwrap();
assert!(
result.function_count >= 3,
"Expected at least 3 functions from attr_* declarations, got {}",
result.function_count
);
}
#[test]
fn test_ruby_require_patterns() {
let parser = ParserManager::new();
let source = r#"
require 'json'
require_relative 'lib/helper'
"#;
let tree = parser.parse(source, "ruby").unwrap();
let result = ElementExtractor::extract_elements(&tree, source, "ruby").unwrap();
assert_eq!(
result.import_count, 2,
"Should find both require and require_relative"
);
}
#[test]
fn test_ruby_method_calls() {
let parser = ParserManager::new();
let source = r#"
class Example
def test_method
puts "Hello"
JSON.parse("{}")
object.method_call
end
end
"#;
let tree = parser.parse(source, "ruby").unwrap();
let result =
ElementExtractor::extract_with_depth(&tree, source, "ruby", "semantic", None).unwrap();
assert!(!result.calls.is_empty(), "Should find method calls");
assert!(result.calls.iter().any(|c| c.callee_name == "puts"));
}
#[test]
fn test_ruby_reference_tracking() {
let parser = ParserManager::new();
let source = r#"
class User
attr_accessor :name
def initialize(name)
@name = name
end
def greet
puts "Hello, #{@name}"
end
end
class Post
STATUS_DRAFT = "draft"
STATUS_PUBLISHED = "published"
def initialize(title)
@title = title
@status = STATUS_DRAFT
end
def publish
@status = STATUS_PUBLISHED
notify_users(@status)
end
end
def main
user = User.new("Alice")
post = Post.new("My Title")
post.publish
end
"#;
let tree = parser.parse(source, "ruby").unwrap();
let result =
ElementExtractor::extract_with_depth(&tree, source, "ruby", "semantic", None).unwrap();
assert_eq!(result.class_count, 2);
let class_names: HashSet<_> = result.classes.iter().map(|c| c.name.as_str()).collect();
assert!(class_names.contains("User"));
assert!(class_names.contains("Post"));
assert!(result.function_count > 0);
let method_names: HashSet<_> = result.functions.iter().map(|f| f.name.as_str()).collect();
assert!(method_names.contains("initialize"));
assert!(method_names.contains("greet"));
assert!(method_names.contains("publish"));
let constant_refs: Vec<_> = result
.references
.iter()
.filter(|r| r.symbol == "STATUS_DRAFT" || r.symbol == "STATUS_PUBLISHED")
.collect();
assert!(
!constant_refs.is_empty(),
"Expected to find constant references"
);
let instantiations: Vec<_> = result
.references
.iter()
.filter(|r| r.ref_type == ReferenceType::TypeInstantiation)
.collect();
assert!(
instantiations.len() >= 2,
"Expected at least 2 class instantiations (User.new, Post.new)"
);
let instantiated_types: HashSet<_> =
instantiations.iter().map(|r| r.symbol.as_str()).collect();
assert!(instantiated_types.contains("User"));
assert!(instantiated_types.contains("Post"));
let constant_usages: Vec<_> = result
.references
.iter()
.filter(|r| r.symbol == "STATUS_DRAFT" || r.symbol == "STATUS_PUBLISHED")
.collect();
assert!(
!constant_usages.is_empty(),
"Expected to find STATUS_* constant usages"
);
}
#[test]
fn test_ruby_call_chains() {
let parser = ParserManager::new();
let file1 = r#"
class User
def initialize(name)
@name = name
end
def display
format_output(@name)
end
def format_output(text)
"User: #{text}"
end
end
"#;
let file2 = r#"
require_relative 'user'
def create_user(name)
User.new(name)
end
def show_user(name)
user = create_user(name)
user.display
end
"#;
let tree1 = parser.parse(file1, "ruby").unwrap();
let result1 =
ElementExtractor::extract_with_depth(&tree1, file1, "ruby", "semantic", None).unwrap();
let tree2 = parser.parse(file2, "ruby").unwrap();
let result2 =
ElementExtractor::extract_with_depth(&tree2, file2, "ruby", "semantic", None).unwrap();
let results = vec![
(PathBuf::from("user.rb"), result1),
(PathBuf::from("main.rb"), result2),
];
let graph = CallGraph::build_from_results(&results);
let incoming_user = graph.find_incoming_chains("User", 1);
assert!(
!incoming_user.is_empty(),
"Expected incoming references to User class"
);
let outgoing_display = graph.find_outgoing_chains("display", 1);
assert!(
!outgoing_display.is_empty(),
"Expected display to call format_output"
);
let outgoing_create = graph.find_outgoing_chains("create_user", 2);
assert!(
!outgoing_create.is_empty(),
"Expected create_user to have call chains"
);
let incoming_create = graph.find_incoming_chains("create_user", 1);
assert!(
!incoming_create.is_empty(),
"Expected show_user to call create_user"
);
}
}
@@ -2,13 +2,10 @@ use rmcp::schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
/// Parameters for the analyze tool
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct AnalyzeParams {
/// Absolute path. Step 1: Directory for overview. Step 2: File for details. Step 3: Directory with focus param for call graphs
pub path: String,
/// Symbol name for call graph analysis (Step 3). Requires directory path with broad enough scope to capture all relevant symbol references
pub focus: Option<String>,
/// Call graph depth. 0=where defined, 1=direct callers/callees, 2+=transitive chains
@@ -19,6 +16,10 @@ pub struct AnalyzeParams {
#[serde(default = "default_max_depth")]
pub max_depth: u32,
/// Maximum depth for recursive AST traversal (prevents stack overflow in deeply nested code)
#[serde(default)]
pub ast_recursion_limit: Option<usize>,
/// Allow large outputs without warning (default: false)
#[serde(default)]
pub force: bool,
@@ -64,11 +65,11 @@ pub struct ClassInfo {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CallInfo {
pub caller_name: Option<String>, // Function containing this call
pub callee_name: String, // Function being called
pub caller_name: Option<String>,
pub callee_name: String,
pub line: usize,
pub column: usize,
pub context: String, // Line of code containing the call
pub context: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -77,14 +78,29 @@ pub struct ReferenceInfo {
pub ref_type: ReferenceType,
pub line: usize,
pub context: String,
/// For method definitions, this stores the type the method belongs to
/// For type usage, this is None
pub associated_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum ReferenceType {
/// Type/class/struct definition
Definition,
/// Method or function definition on a type (use associated_type to link to type)
MethodDefinition,
/// Function call or method call
Call,
/// Type instantiation (e.g., struct literal, class constructor)
TypeInstantiation,
/// Type used in field declaration
FieldType,
/// Type used in variable declaration
VariableType,
/// Type used in function/method parameter
ParameterType,
/// Import statement
Import,
Assignment,
}
// Entry type for directory results - cleaner than overloading AnalysisResult