Developer analyze tool improvement (#5030)
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, ¶ms, &traverser)?,
|
||||
AnalysisMode::Semantic => {
|
||||
if path.is_file() {
|
||||
let result = self.analyze_file(&path, &mode)?;
|
||||
let result = self.analyze_file(&path, &mode, ¶ms)?;
|
||||
Formatter::format_analysis_result(&path, &result, &mode)
|
||||
} else {
|
||||
// Semantic mode on directory - analyze all files
|
||||
self.analyze_directory(&path, ¶ms, &traverser, &mode)?
|
||||
}
|
||||
}
|
||||
AnalysisMode::Structure => {
|
||||
if path.is_file() {
|
||||
let result = self.analyze_file(&path, &mode)?;
|
||||
let result = self.analyze_file(&path, &mode, ¶ms)?;
|
||||
Formatter::format_analysis_result(&path, &result, &mode)
|
||||
} else {
|
||||
self.analyze_directory(&path, ¶ms, &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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user