use std::collections::HashMap;
use std::fmt;
use std::path::Path;
use serde::{Serialize, Serializer};
use crate::builder::parse_sorbet_type;
use crate::parser::parse_tree::MethodLoc;
#[derive(Debug, Default, Serialize)]
pub struct Project {
pub classes: HashMap<String, ClassInfo>,
pub methods: Vec<MethodInfo>,
}
impl Project {
pub fn find_methods(&self, query: &str) -> Vec<&MethodInfo> {
if let Some((class_part, method_part)) = query.split_once('#') {
let direct: Vec<_> = self
.methods
.iter()
.filter(|m| {
m.fqn.kind == MethodKind::Instance
&& m.fqn.method_name == method_part
&& m.fqn.class_fqn.ends_with(class_part)
})
.collect();
if !direct.is_empty() {
return direct;
}
self.find_inherited_method(class_part, method_part, MethodKind::Instance)
} else if let Some((class_part, method_part)) = query.split_once('.') {
let direct: Vec<_> = self
.methods
.iter()
.filter(|m| {
m.fqn.kind == MethodKind::Class
&& m.fqn.method_name == method_part
&& m.fqn.class_fqn.ends_with(class_part)
})
.collect();
if !direct.is_empty() {
return direct;
}
self.find_inherited_method(class_part, method_part, MethodKind::Class)
} else {
self.methods
.iter()
.filter(|m| m.fqn.class_fqn.ends_with(query))
.collect()
}
}
fn find_inherited_method(
&self,
class_part: &str,
method_name: &str,
kind: MethodKind,
) -> Vec<&MethodInfo> {
let matching_classes: Vec<_> = self
.classes
.values()
.filter(|c| c.fqn.ends_with(class_part))
.collect();
for class in matching_classes {
let mut current_super = class.super_class.as_deref();
let mut visited = std::collections::HashSet::new();
while let Some(super_fqn) = current_super {
if !visited.insert(super_fqn.to_string()) {
break;
}
let results: Vec<_> = self
.methods
.iter()
.filter(|m| {
m.fqn.kind == kind
&& m.fqn.method_name == method_name
&& m.fqn.class_fqn == super_fqn
})
.collect();
if !results.is_empty() {
return results;
}
current_super = self
.classes
.get(super_fqn)
.and_then(|c| c.super_class.as_deref());
}
}
Vec::new()
}
pub fn find_classes(&self, query: &str) -> Vec<&ClassInfo> {
self.classes
.values()
.filter(|c| c.fqn.ends_with(query))
.collect()
}
pub fn resolve_source_locations(&mut self, project_root: &Path) {
for method in &mut self.methods {
let class_file = self
.classes
.get(&method.fqn.class_fqn)
.and_then(|c| c.file_path.as_ref());
let Some(rel_path) = class_file else {
continue;
};
let full_path = project_root.join(rel_path);
let Ok(content) = std::fs::read_to_string(&full_path) else {
continue;
};
let def_pattern = match method.fqn.kind {
MethodKind::Instance => format!("def {}", method.fqn.method_name),
MethodKind::Class => format!("def self.{}", method.fqn.method_name),
};
for (i, line) in content.lines().enumerate() {
if line.trim_start().starts_with(&def_pattern) {
method.file_path = Some(rel_path.clone());
method.line = Some(i + 1);
break;
}
}
}
}
pub fn resolve_source_locations_from_locs(&mut self, locs: &[MethodLoc]) {
for method in &mut self.methods {
let class_file = self
.classes
.get(&method.fqn.class_fqn)
.and_then(|c| c.file_path.as_ref());
let Some(rel_path) = class_file else {
continue;
};
let is_class = method.fqn.kind == MethodKind::Class;
let rel_normalized = rel_path.strip_prefix("./").unwrap_or(rel_path);
if let Some(loc) = locs.iter().find(|l| {
let loc_file = l.file.strip_prefix("./").unwrap_or(&l.file);
loc_file == rel_normalized
&& l.name == method.fqn.method_name
&& l.is_class_method == is_class
}) {
method.file_path = Some(rel_path.clone());
method.line = Some(loc.line);
if let Some(ref sig_ret) = loc.sig_return_type {
method.return_type = Some(parse_sorbet_type(sig_ret));
}
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct MethodFqn {
pub class_fqn: String,
pub method_name: String,
pub kind: MethodKind,
}
impl Serialize for MethodFqn {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_string())
}
}
impl fmt::Display for MethodFqn {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let sep = match self.kind {
MethodKind::Instance => "#",
MethodKind::Class => ".",
};
write!(f, "{}{}{}", self.class_fqn, sep, self.method_name)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
pub enum MethodKind {
Instance,
Class,
}
#[derive(Debug, Clone, Serialize)]
pub struct ClassInfo {
pub fqn: String,
pub is_module: bool,
pub super_class: Option<String>,
pub mixins: Vec<String>,
#[serde(skip)]
pub method_fqns: Vec<MethodFqn>,
pub file_path: Option<String>,
pub line: Option<usize>,
}
#[derive(Debug, Serialize)]
pub struct MethodInfo {
pub fqn: MethodFqn,
pub file_path: Option<String>,
pub line: Option<usize>,
pub arguments: Vec<Argument>,
pub return_type: Option<SorbetType>,
pub calls: Vec<MethodCall>,
pub ivars: Vec<IvarAccess>,
pub rescues: Vec<String>,
pub uses_block: bool,
#[serde(skip)]
pub basic_blocks: Vec<BasicBlock>,
}
#[derive(Debug)]
pub struct BasicBlock {
pub id: usize,
pub terminator: Terminator,
}
#[derive(Debug)]
pub enum Terminator {
Goto(usize),
Branch {
condition: String,
true_bb: usize,
false_bb: usize,
},
BlockCall { true_bb: usize, false_bb: usize },
Return,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ArgumentKind {
Req,
Opt,
Rest,
KeyReq,
Key,
KeyRest,
Block,
}
#[derive(Debug, Clone, Serialize)]
pub struct Argument {
pub name: String,
pub ty: SorbetType,
pub kind: ArgumentKind,
}
#[derive(Debug, Clone, Serialize)]
pub struct MethodCall {
pub receiver_type: SorbetType,
pub method_name: String,
pub return_type: SorbetType,
pub target_fqn: Option<String>,
pub bb_id: usize,
pub conditions: Vec<BranchCondition>,
}
#[derive(Debug, Clone, Serialize)]
pub struct BranchCondition {
pub call: String,
pub is_true: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct IvarAccess {
pub name: String,
pub ty: SorbetType,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SorbetType {
Simple(String),
Nilable(Box<SorbetType>),
Union(Vec<SorbetType>),
Array(Box<SorbetType>),
Hash(Box<SorbetType>, Box<SorbetType>),
ClassOf(String),
Tuple(Vec<SorbetType>),
Shape(Vec<(String, SorbetType)>),
Literal(String),
Boolean,
Untyped,
NoReturn,
Void,
}
impl Serialize for SorbetType {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_string())
}
}
impl fmt::Display for SorbetType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SorbetType::Simple(s) => write!(f, "{s}"),
SorbetType::Nilable(inner) => write!(f, "T.nilable({inner})"),
SorbetType::Union(types) => {
let parts: Vec<_> = types.iter().map(|t| t.to_string()).collect();
write!(f, "T.any({})", parts.join(", "))
}
SorbetType::Array(inner) => write!(f, "T::Array[{inner}]"),
SorbetType::Hash(k, v) => write!(f, "T::Hash[{k}, {v}]"),
SorbetType::ClassOf(name) => write!(f, "T.class_of({name})"),
SorbetType::Tuple(types) => {
let parts: Vec<_> = types.iter().map(|t| t.to_string()).collect();
write!(f, "[{}]", parts.join(", "))
}
SorbetType::Shape(fields) => {
let parts: Vec<_> = fields.iter().map(|(k, v)| format!("{k}: {v}")).collect();
write!(f, "{{{}}}", parts.join(", "))
}
SorbetType::Literal(s) => write!(f, "{s}"),
SorbetType::Boolean => write!(f, "T::Boolean"),
SorbetType::Untyped => write!(f, "T.untyped"),
SorbetType::NoReturn => write!(f, "T.noreturn"),
SorbetType::Void => write!(f, "void"),
}
}
}