code-analyze 0.1.1

Analyze code structure and relationships using tree-sitter
Documentation
// Copyright 2024 Block, Inc. (original code from https://github.com/block/goose)
// Copyright 2025 utapyngo (modifications)
// SPDX-License-Identifier: Apache-2.0

/// Tree-sitter query for extracting Ruby code elements.
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.
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.
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
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);

    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() as u32 {
        if let Some(child) = class_node.child(i)
            && 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() as u32 {
        if let Some(child) = node.child(i)
            && child.kind() == "method"
        {
            for j in 0..child.child_count() as u32 {
                if let Some(name_node) = child.child(j)
                    && name_node.kind() == "identifier"
                {
                    return source.get(name_node.byte_range()).map(|s| s.to_string());
                }
            }
        }
    }
    None
}