#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
#![allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
use super::npa::{
RubyVisibility, RubyVisibilityCall, RubyVisibilityEffect, ruby_call_named_arguments,
ruby_declaration_is_public, ruby_method_name, ruby_symbol_name, ruby_visibility_effect,
ruby_visibility_marker, ruby_wrapped_is_public,
};
use super::*;
const RUBY_AUTO_PRIVATE_METHODS: [&str; 5] = [
"initialize",
"initialize_copy",
"initialize_dup",
"initialize_clone",
"respond_to_missing?",
];
struct RubyMethodDecl<'a> {
name: Option<&'a str>,
singleton: bool,
public: bool,
}
struct RubyClassBody<'a> {
code: &'a [u8],
in_singleton_class: bool,
visibility: RubyVisibility,
methods: Vec<RubyMethodDecl<'a>>,
}
impl<'a> RubyClassBody<'a> {
fn new(code: &'a [u8], in_singleton_class: bool) -> Self {
Self {
code,
in_singleton_class,
visibility: RubyVisibility::Public,
methods: Vec::new(),
}
}
fn is_auto_private(&self, name: Option<&str>, singleton: bool) -> bool {
!singleton
&& !self.in_singleton_class
&& name.is_some_and(|n| RUBY_AUTO_PRIVATE_METHODS.contains(&n))
}
fn declare(&mut self, method: &Node<'a>, singleton: bool, keyword: Option<RubyVisibilityCall>) {
let name = ruby_method_name(method, self.code);
let named_by_keyword = keyword.is_some_and(|kw| kw.governs(singleton));
let public = keyword.map_or_else(
|| ruby_declaration_is_public(singleton, self.visibility),
|kw| ruby_wrapped_is_public(kw, singleton, self.visibility),
);
self.methods.push(RubyMethodDecl {
name,
singleton,
public: public && (named_by_keyword || !self.is_auto_private(name, singleton)),
});
}
fn refile(&mut self, name: &str, singleton: bool, public: bool) {
for target in self
.methods
.iter_mut()
.filter(|m| m.singleton == singleton && m.name == Some(name))
{
target.public = public;
}
}
fn refile_argument(&mut self, arg: &Node<'a>, singleton: bool, public: bool) {
let code = self.code;
if matches!(arg.kind_id().into(), Ruby::SymbolArray) {
for name in arg
.children()
.filter_map(|sym| ruby_symbol_name(&sym, code))
{
self.refile(name, singleton, public);
}
} else if let Some(name) = ruby_symbol_name(arg, code) {
self.refile(name, singleton, public);
}
}
fn apply_visibility_call(&mut self, call: &Node<'a>, keyword: RubyVisibilityCall) {
use Ruby::*;
let public = keyword.visibility == RubyVisibility::Public;
for arg in ruby_call_named_arguments(call) {
let singleton = match arg.kind_id().into() {
Method => false,
SingletonMethod => true,
_ => {
self.refile_argument(&arg, keyword.targets_singleton, public);
continue;
}
};
self.declare(&arg, singleton, Some(keyword));
}
}
fn visit(&mut self, child: &Node<'a>) {
use Ruby::*;
if let Some(marker) = ruby_visibility_marker(child, self.code) {
self.visibility = marker;
return;
}
let kind = child.kind_id().into();
match kind {
Method | SingletonMethod => {
self.declare(child, matches!(kind, SingletonMethod), None);
}
Call | Call2 | Call3 | Call4 => match ruby_visibility_effect(child, self.code) {
Some(RubyVisibilityEffect::Flag(flag)) => self.visibility = flag,
Some(RubyVisibilityEffect::Arguments(keyword)) => {
self.apply_visibility_call(child, keyword);
}
None => {}
},
_ => {}
}
}
}
impl Npm for RubyCode {
fn compute<'a>(
node: &Node<'a>,
code: &'a [u8],
ancestors: Ancestors<'a, '_>,
stats: &mut Stats,
) {
use Ruby::*;
if !matches!(node.kind_id().into(), BodyStatement | BodyStatement2) {
return;
}
let Some(parent_kind) = ancestors.parent(node).map(|p| p.kind_id().into()) else {
return;
};
if !matches!(parent_kind, Class | SingletonClass) {
return;
}
let mut body = RubyClassBody::new(code, matches!(parent_kind, SingletonClass));
for child in node.children() {
body.visit(&child);
}
stats.class_nm += body.methods.len();
stats.class_npm += body.methods.iter().filter(|m| m.public).count();
}
}