use crate::error::{Error, Result};
use crate::parser::doc_comment::{parse_yardoc, ParsedDocComment};
use crate::parser::Parser;
use crate::schema::*;
use std::path::Path;
use tree_sitter::{Node, Parser as TsParser, Tree};
pub struct RubyParser {
ts_parser: TsParser,
source: String,
file_path: String,
context_stack: Vec<String>,
}
impl RubyParser {
pub fn new() -> Result<Self> {
let mut ts_parser = TsParser::new();
ts_parser
.set_language(&tree_sitter_ruby::language())
.map_err(|e| Error::tree_sitter(format!("Failed to load Ruby grammar: {}", e)))?;
Ok(Self {
ts_parser,
source: String::new(),
file_path: String::new(),
context_stack: Vec::new(),
})
}
pub fn parse(&mut self, source: &str) -> Result<Tree> {
self.source = source.to_string();
self.ts_parser
.parse(source, None)
.ok_or_else(|| Error::tree_sitter("Failed to parse Ruby source"))
}
fn current_prefix(&self) -> String {
self.context_stack.join("::")
}
fn make_id(&self, name: &str, separator: &str) -> String {
let prefix = self.current_prefix();
if prefix.is_empty() {
name.to_string()
} else {
format!("{}{}{}", prefix, separator, name)
}
}
fn node_text(&self, node: Node) -> &str {
node.utf8_text(self.source.as_bytes()).unwrap_or("")
}
fn node_location(&self, node: Node) -> SourceLocation {
let point = node.start_position();
SourceLocation {
file: self.file_path.clone(),
line: point.row as u32 + 1,
column: Some(point.column as u32),
}
}
fn find_preceding_comment(&self, node: Node) -> Option<String> {
if let Some(comment) = self.find_comment_in_siblings(node) {
return Some(comment);
}
if node.prev_sibling().is_none() {
if let Some(parent) = node.parent() {
if parent.kind() == "body_statement" && parent.parent().is_some() {
return self.find_comment_in_siblings(parent);
}
}
}
None
}
fn find_comment_in_siblings(&self, node: Node) -> Option<String> {
let mut current = node;
while let Some(prev) = current.prev_sibling() {
match prev.kind() {
"comment" => {
let mut comments = vec![self.node_text(prev).to_string()];
let mut check = prev;
while let Some(prev_prev) = check.prev_sibling() {
if prev_prev.kind() == "comment" {
comments.insert(0, self.node_text(prev_prev).to_string());
check = prev_prev;
} else {
break;
}
}
return Some(comments.join("\n"));
}
_ if prev.is_named() => {
break;
}
_ => {
current = prev;
}
}
}
None
}
fn parse_module(&mut self, node: Node) -> Option<Module> {
let name_node = node.child_by_field_name("name")?;
let name = self.node_text(name_node).to_string();
let id = self.make_id(&name, "::");
let doc_comment = self.find_preceding_comment(node);
let parsed_doc = doc_comment.as_ref().map(|c| parse_yardoc(c));
let mut module = Module {
base: BaseEntity {
id: id.clone(),
name: name.clone(),
location: Some(self.node_location(node)),
docs: parsed_doc.as_ref().map(|d| d.to_documentation()),
visibility: Some(Visibility::Public),
is_private_api: parsed_doc.as_ref().and_then(|d| {
if d.is_private {
Some(true)
} else {
None
}
}),
},
extends: Vec::new(),
includes: Vec::new(),
functions: Vec::new(),
properties: Vec::new(),
constants: Vec::new(),
classes: Vec::new(),
modules: Vec::new(),
interfaces: Vec::new(),
types: Vec::new(),
enums: Vec::new(),
is_concern: None,
instance_methods: Vec::new(),
class_methods: Vec::new(),
};
self.context_stack.push(name.clone());
if let Some(body) = node.child_by_field_name("body") {
self.parse_module_body(&mut module, body);
}
self.context_stack.pop();
Some(module)
}
fn parse_module_body(&mut self, module: &mut Module, body: Node) {
let mut cursor = body.walk();
for child in body.children(&mut cursor) {
match child.kind() {
"module" => {
if let Some(nested) = self.parse_module(child) {
module.modules.push(nested);
}
}
"class" => {
if let Some(class) = self.parse_class(child) {
module.classes.push(class);
}
}
"method" => {
if let Some(method) = self.parse_method(child, false) {
module.instance_methods.push(method);
}
}
"singleton_method" => {
if let Some(method) = self.parse_singleton_method(child) {
module.class_methods.push(method);
}
}
"constant_assignment" | "assignment" => {
if let Some(constant) = self.parse_constant(child) {
module.constants.push(constant);
}
}
"call" => {
self.parse_module_call(module, child);
}
_ => {}
}
}
}
fn parse_module_call(&mut self, module: &mut Module, node: Node) {
let method_node = node.child_by_field_name("method");
let method_name = method_node.map(|n| self.node_text(n));
match method_name {
Some("include") => {
if let Some(args) = node.child_by_field_name("arguments") {
let mut cursor = args.walk();
for arg in args.children(&mut cursor) {
if arg.is_named() {
let type_name = self.node_text(arg).to_string();
module.includes.push(TypeReference::simple(type_name));
}
}
}
}
Some("extend") => {
if let Some(args) = node.child_by_field_name("arguments") {
let type_name = self.node_text(args).to_string();
if type_name.contains("ActiveSupport::Concern") {
module.is_concern = Some(true);
} else {
module.extends.push(TypeReference::simple(type_name));
}
}
}
Some("attr_reader") | Some("attr_accessor") | Some("attr_writer") => {
if let Some(args) = node.child_by_field_name("arguments") {
let is_reader = method_name == Some("attr_reader");
let is_writer = method_name == Some("attr_writer");
let is_accessor = method_name == Some("attr_accessor");
let mut cursor = args.walk();
for arg in args.children(&mut cursor) {
if arg.kind() == "simple_symbol" || arg.kind() == "symbol" {
let attr_name = self.node_text(arg).trim_start_matches(':').to_string();
let id = self.make_id(&attr_name, "#");
module.properties.push(Property {
base: BaseEntity {
id,
name: attr_name,
location: Some(self.node_location(arg)),
docs: None,
visibility: Some(Visibility::Public),
is_private_api: None,
},
prop_type: None,
default: None,
readonly: if is_reader { Some(true) } else { None },
is_static: None,
optional: None,
getter: if is_reader || is_accessor {
Some(true)
} else {
None
},
setter: if is_writer || is_accessor {
Some(true)
} else {
None
},
});
}
}
}
}
_ => {}
}
}
fn parse_class(&mut self, node: Node) -> Option<Class> {
let name_node = node.child_by_field_name("name")?;
let name = self.node_text(name_node).to_string();
let id = self.make_id(&name, "::");
let doc_comment = self.find_preceding_comment(node);
let parsed_doc = doc_comment.as_ref().map(|c| parse_yardoc(c));
let superclass = node.child_by_field_name("superclass").map(|n| {
let name = self
.node_text(n)
.trim()
.strip_prefix('<')
.unwrap_or(self.node_text(n))
.trim()
.to_string();
TypeReference::simple(name)
});
let mut class = Class {
base: BaseEntity {
id: id.clone(),
name: name.clone(),
location: Some(self.node_location(node)),
docs: parsed_doc.as_ref().map(|d| d.to_documentation()),
visibility: Some(Visibility::Public),
is_private_api: parsed_doc.as_ref().and_then(|d| {
if d.is_private {
Some(true)
} else {
None
}
}),
},
extends: superclass,
implements: Vec::new(),
includes: Vec::new(),
prepends: Vec::new(),
type_params: Vec::new(),
is_abstract: None,
constructor: None,
properties: Vec::new(),
methods: Vec::new(),
static_properties: Vec::new(),
static_methods: Vec::new(),
constants: Vec::new(),
nested: Vec::new(),
};
self.context_stack.push(name.clone());
if let Some(body) = node.child_by_field_name("body") {
self.parse_class_body(&mut class, body);
}
self.context_stack.pop();
Some(class)
}
fn parse_class_body(&mut self, class: &mut Class, body: Node) {
let mut cursor = body.walk();
let mut current_visibility = Visibility::Public;
for child in body.children(&mut cursor) {
match child.kind() {
"identifier" => {
let text = self.node_text(child);
match text {
"private" => current_visibility = Visibility::Private,
"protected" => current_visibility = Visibility::Protected,
"public" => current_visibility = Visibility::Public,
_ => {}
}
}
"method" => {
if let Some(mut method) = self.parse_method(child, false) {
method.base.visibility = Some(current_visibility);
if method.base.name == "initialize" {
class.constructor = Some(Box::new(method));
} else {
class.methods.push(method);
}
}
}
"singleton_method" => {
if let Some(method) = self.parse_singleton_method(child) {
class.static_methods.push(method);
}
}
"constant_assignment" | "assignment" => {
if let Some(constant) = self.parse_constant(child) {
class.constants.push(constant);
}
}
"call" => {
let method_node = child.child_by_field_name("method");
match method_node.map(|n| self.node_text(n)) {
Some("private") => current_visibility = Visibility::Private,
Some("protected") => current_visibility = Visibility::Protected,
Some("public") => current_visibility = Visibility::Public,
Some("include") => {
if let Some(args) = child.child_by_field_name("arguments") {
let mut arg_cursor = args.walk();
for arg in args.children(&mut arg_cursor) {
if arg.is_named() {
let type_name = self.node_text(arg).to_string();
class.includes.push(TypeReference::simple(type_name));
}
}
}
}
Some("prepend") => {
if let Some(args) = child.child_by_field_name("arguments") {
let mut arg_cursor = args.walk();
for arg in args.children(&mut arg_cursor) {
if arg.is_named() {
let type_name = self.node_text(arg).to_string();
class.prepends.push(TypeReference::simple(type_name));
}
}
}
}
Some("attr_reader") | Some("attr_accessor") | Some("attr_writer") => {
let method_name_str = method_node.map(|n| self.node_text(n));
let is_reader = method_name_str == Some("attr_reader");
let is_writer = method_name_str == Some("attr_writer");
let is_accessor = method_name_str == Some("attr_accessor");
if let Some(args) = child.child_by_field_name("arguments") {
let mut arg_cursor = args.walk();
for arg in args.children(&mut arg_cursor) {
if arg.kind() == "simple_symbol" || arg.kind() == "symbol" {
let attr_name =
self.node_text(arg).trim_start_matches(':').to_string();
let id = self.make_id(&attr_name, "#");
class.properties.push(Property {
base: BaseEntity {
id,
name: attr_name,
location: Some(self.node_location(arg)),
docs: None,
visibility: Some(current_visibility),
is_private_api: None,
},
prop_type: None,
default: None,
readonly: if is_reader { Some(true) } else { None },
is_static: None,
optional: None,
getter: if is_reader || is_accessor {
Some(true)
} else {
None
},
setter: if is_writer || is_accessor {
Some(true)
} else {
None
},
});
}
}
}
}
_ => {}
}
}
"class" => {
if let Some(nested_class) = self.parse_class(child) {
class.nested.push(Entity::Class(nested_class));
}
}
"module" => {
if let Some(nested_module) = self.parse_module(child) {
class.nested.push(Entity::Module(nested_module));
}
}
_ => {}
}
}
}
fn parse_method(&mut self, node: Node, is_static: bool) -> Option<Callable> {
let name_node = node.child_by_field_name("name")?;
let name = self.node_text(name_node).to_string();
let separator = if is_static { "." } else { "#" };
let id = self.make_id(&name, separator);
let doc_comment = self.find_preceding_comment(node);
let parsed_doc = doc_comment.as_ref().map(|c| parse_yardoc(c));
let params = self.parse_method_params(node);
Some(Callable {
base: BaseEntity {
id,
name,
location: Some(self.node_location(node)),
docs: parsed_doc.as_ref().map(|d| d.to_documentation()),
visibility: Some(Visibility::Public),
is_private_api: parsed_doc.as_ref().and_then(|d| {
if d.is_private {
Some(true)
} else {
None
}
}),
},
params: merge_params(params, parsed_doc.as_ref()),
returns: parsed_doc.as_ref().and_then(|d| d.return_type()),
returns_description: parsed_doc.as_ref().and_then(|d| d.return_description()),
throws: parsed_doc
.as_ref()
.map(|d| d.to_throws())
.unwrap_or_default(),
yields: parsed_doc.as_ref().and_then(|d| d.to_yield_block()),
is_async: None,
is_static: if is_static { Some(true) } else { None },
is_abstract: None,
generator: None,
type_params: Vec::new(),
overloads: Vec::new(),
})
}
fn parse_singleton_method(&mut self, node: Node) -> Option<Callable> {
let name_node = node.child_by_field_name("name")?;
let name = self.node_text(name_node).to_string();
let id = self.make_id(&name, ".");
let doc_comment = self.find_preceding_comment(node);
let parsed_doc = doc_comment.as_ref().map(|c| parse_yardoc(c));
let params = self.parse_method_params(node);
Some(Callable {
base: BaseEntity {
id,
name,
location: Some(self.node_location(node)),
docs: parsed_doc.as_ref().map(|d| d.to_documentation()),
visibility: Some(Visibility::Public),
is_private_api: parsed_doc.as_ref().and_then(|d| {
if d.is_private {
Some(true)
} else {
None
}
}),
},
params: merge_params(params, parsed_doc.as_ref()),
returns: parsed_doc.as_ref().and_then(|d| d.return_type()),
returns_description: parsed_doc.as_ref().and_then(|d| d.return_description()),
throws: parsed_doc
.as_ref()
.map(|d| d.to_throws())
.unwrap_or_default(),
yields: parsed_doc.as_ref().and_then(|d| d.to_yield_block()),
is_async: None,
is_static: Some(true),
is_abstract: None,
generator: None,
type_params: Vec::new(),
overloads: Vec::new(),
})
}
fn parse_method_params(&self, node: Node) -> Vec<Parameter> {
let mut params = Vec::new();
if let Some(params_node) = node.child_by_field_name("parameters") {
let mut cursor = params_node.walk();
for child in params_node.children(&mut cursor) {
match child.kind() {
"identifier" => {
params.push(Parameter {
name: self.node_text(child).to_string(),
param_type: None,
description: None,
default: None,
optional: None,
rest: None,
options: Vec::new(),
});
}
"optional_parameter" => {
let name = child
.child_by_field_name("name")
.map(|n| self.node_text(n).to_string())
.unwrap_or_default();
let default = child
.child_by_field_name("value")
.map(|n| self.node_text(n).to_string());
params.push(Parameter {
name,
param_type: None,
description: None,
default,
optional: Some(true),
rest: None,
options: Vec::new(),
});
}
"splat_parameter" => {
let name = child
.child_by_field_name("name")
.map(|n| self.node_text(n).to_string())
.unwrap_or_else(|| "args".to_string());
params.push(Parameter {
name,
param_type: None,
description: None,
default: None,
optional: None,
rest: Some(true),
options: Vec::new(),
});
}
"keyword_parameter" => {
let name = child
.child_by_field_name("name")
.map(|n| self.node_text(n).trim_end_matches(':').to_string())
.unwrap_or_default();
let default = child
.child_by_field_name("value")
.map(|n| self.node_text(n).to_string());
let has_default = default.is_some();
params.push(Parameter {
name,
param_type: None,
description: None,
default,
optional: Some(has_default),
rest: None,
options: Vec::new(),
});
}
"block_parameter" => {
let name = child
.child_by_field_name("name")
.map(|n| self.node_text(n).to_string())
.unwrap_or_else(|| "block".to_string());
params.push(Parameter {
name: format!("&{}", name),
param_type: Some(TypeReference::simple("Proc")),
description: None,
default: None,
optional: Some(true),
rest: None,
options: Vec::new(),
});
}
_ => {}
}
}
}
params
}
fn parse_constant(&self, node: Node) -> Option<Constant> {
let left = node.child_by_field_name("left")?;
let name = self.node_text(left).to_string();
if !name
.chars()
.next()
.map(|c| c.is_uppercase())
.unwrap_or(false)
{
return None;
}
let id = self.make_id(&name, "::");
let right = node.child_by_field_name("right");
let value = right.map(|n| self.node_text(n).to_string());
let doc_comment = self.find_preceding_comment(node);
let parsed_doc = doc_comment.as_ref().map(|c| parse_yardoc(c));
Some(Constant {
base: BaseEntity {
id,
name,
location: Some(self.node_location(node)),
docs: parsed_doc.as_ref().map(|d| d.to_documentation()),
visibility: Some(Visibility::Public),
is_private_api: parsed_doc.as_ref().and_then(|d| {
if d.is_private {
Some(true)
} else {
None
}
}),
},
const_type: None,
value,
})
}
fn generate_module_symbols(&self, module: &Module, parent: Option<&str>) -> Vec<SymbolEntry> {
let mut symbols = vec![SymbolEntry {
id: module.base.id.clone(),
name: module.base.name.clone(),
qualified_name: module.base.id.clone(),
kind: "module".to_string(),
parent: parent.map(|s| s.to_string()),
file: module.base.location.as_ref().map(|l| l.file.clone()),
line: module.base.location.as_ref().map(|l| l.line),
signature: None,
}];
let parent_id = &module.base.id;
for constant in &module.constants {
symbols.push(SymbolEntry {
id: constant.base.id.clone(),
name: constant.base.name.clone(),
qualified_name: constant.base.id.clone(),
kind: "constant".to_string(),
parent: Some(parent_id.clone()),
file: constant.base.location.as_ref().map(|l| l.file.clone()),
line: constant.base.location.as_ref().map(|l| l.line),
signature: None,
});
}
for method in &module.instance_methods {
symbols.push(self.callable_to_symbol(method, Some(parent_id)));
}
for method in &module.class_methods {
symbols.push(self.callable_to_symbol(method, Some(parent_id)));
}
for nested in &module.modules {
symbols.extend(self.generate_module_symbols(nested, Some(parent_id)));
}
for class in &module.classes {
symbols.extend(self.generate_class_symbols(class, Some(parent_id)));
}
symbols
}
fn generate_class_symbols(&self, class: &Class, parent: Option<&str>) -> Vec<SymbolEntry> {
let mut symbols = vec![SymbolEntry {
id: class.base.id.clone(),
name: class.base.name.clone(),
qualified_name: class.base.id.clone(),
kind: "class".to_string(),
parent: parent.map(|s| s.to_string()),
file: class.base.location.as_ref().map(|l| l.file.clone()),
line: class.base.location.as_ref().map(|l| l.line),
signature: class
.extends
.as_ref()
.map(|e| format!("{} < {}", class.base.name, e.name)),
}];
let parent_id = &class.base.id;
if let Some(ctor) = &class.constructor {
symbols.push(self.callable_to_symbol(ctor, Some(parent_id)));
}
for prop in &class.properties {
symbols.push(SymbolEntry {
id: prop.base.id.clone(),
name: prop.base.name.clone(),
qualified_name: prop.base.id.clone(),
kind: "property".to_string(),
parent: Some(parent_id.clone()),
file: prop.base.location.as_ref().map(|l| l.file.clone()),
line: prop.base.location.as_ref().map(|l| l.line),
signature: None,
});
}
for method in &class.methods {
symbols.push(self.callable_to_symbol(method, Some(parent_id)));
}
for method in &class.static_methods {
symbols.push(self.callable_to_symbol(method, Some(parent_id)));
}
for constant in &class.constants {
symbols.push(SymbolEntry {
id: constant.base.id.clone(),
name: constant.base.name.clone(),
qualified_name: constant.base.id.clone(),
kind: "constant".to_string(),
parent: Some(parent_id.clone()),
file: constant.base.location.as_ref().map(|l| l.file.clone()),
line: constant.base.location.as_ref().map(|l| l.line),
signature: None,
});
}
for nested in &class.nested {
match nested {
Entity::Class(c) => symbols.extend(self.generate_class_symbols(c, Some(parent_id))),
Entity::Module(m) => {
symbols.extend(self.generate_module_symbols(m, Some(parent_id)))
}
_ => {}
}
}
symbols
}
fn callable_to_symbol(&self, callable: &Callable, parent: Option<&str>) -> SymbolEntry {
let param_names: Vec<String> = callable
.params
.iter()
.map(|p| {
let mut s = p.name.clone();
if p.optional == Some(true) {
if let Some(default) = &p.default {
s.push_str(&format!(" = {}", default));
}
}
s
})
.collect();
let signature = format!("{}({})", callable.base.name, param_names.join(", "));
SymbolEntry {
id: callable.base.id.clone(),
name: callable.base.name.clone(),
qualified_name: callable.base.id.clone(),
kind: if callable.base.name == "initialize" {
"constructor"
} else {
"method"
}
.to_string(),
parent: parent.map(|s| s.to_string()),
file: callable.base.location.as_ref().map(|l| l.file.clone()),
line: callable.base.location.as_ref().map(|l| l.line),
signature: Some(signature),
}
}
}
impl Parser for RubyParser {
fn language(&self) -> Language {
Language::Ruby
}
fn parse_file(&mut self, path: &Path, content: &str) -> Result<Vec<Entity>> {
self.file_path = path.to_string_lossy().to_string();
self.context_stack.clear();
let tree = self.parse(content)?;
let root = tree.root_node();
let mut entities = Vec::new();
let mut cursor = root.walk();
for child in root.children(&mut cursor) {
match child.kind() {
"module" => {
if let Some(module) = self.parse_module(child) {
entities.push(Entity::Module(module));
}
}
"class" => {
if let Some(class) = self.parse_class(child) {
entities.push(Entity::Class(class));
}
}
_ => {}
}
}
Ok(entities)
}
fn generate_symbols(&self, entities: &[Entity], parent: Option<&str>) -> Vec<SymbolEntry> {
let mut symbols = Vec::new();
for entity in entities {
match entity {
Entity::Module(m) => {
symbols.extend(self.generate_module_symbols(m, parent));
}
Entity::Class(c) => {
symbols.extend(self.generate_class_symbols(c, parent));
}
_ => {}
}
}
symbols
}
}
fn merge_params(mut params: Vec<Parameter>, doc: Option<&ParsedDocComment>) -> Vec<Parameter> {
if let Some(doc) = doc {
for param in &mut params {
if let Some(doc_param) = doc.params.iter().find(|p| p.name == param.name) {
if param.param_type.is_none() {
param.param_type = doc_param
.type_str
.as_ref()
.map(|t| crate::parser::doc_comment::parse_type_string(t));
}
if param.description.is_none() {
param.description = doc_param.description.clone();
}
}
let options: Vec<OptionEntry> = doc
.options
.iter()
.filter(|o| o.param_name == param.name)
.map(|o| OptionEntry {
name: o.option_name.clone(),
option_type: o
.type_str
.as_ref()
.map(|t| crate::parser::doc_comment::parse_type_string(t)),
description: o.description.clone(),
required: Some(o.required),
default: o.default.clone(),
})
.collect();
param.options = options;
}
}
params
}