#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
use super::*;
use crate::lang_helpers::doc_comment::{node_bytes, ruby_directive_opener, ruby_magic_comment};
#[must_use]
pub fn is_stabby_lambda_body<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> bool {
ancestors
.parent(node)
.is_some_and(|parent| parent.kind_id() == Ruby::Lambda)
}
fn ruby_documented_core<'a>(node: &Node<'a>) -> Option<Node<'a>> {
let mut node = *node;
loop {
node = match node.kind_id().into() {
Ruby::IfModifier | Ruby::UnlessModifier => node.child_by_field_name("body")?,
Ruby::BodyStatement | Ruby::BodyStatement2 | Ruby::BlockBody => node.child(0)?,
_ => return Some(node),
};
}
}
fn ruby_documented_statement(node: &Node, code: &[u8]) -> bool {
match node.kind_id().into() {
Ruby::Method | Ruby::SingletonMethod | Ruby::Class | Ruby::Module => true,
Ruby::Alias => node
.child_by_field_name("name")
.is_none_or(|name| name.kind_id() != Ruby::GlobalVariable as u16),
Ruby::SingletonClass => node.child_by_field_name("value").is_some_and(|value| {
matches!(
value.kind_id().into(),
Ruby::Constant | Ruby::ScopeResolution | Ruby::ScopeResolution2
)
}),
Ruby::Assignment | Ruby::Assignment2 => {
node.child_by_field_name("left").is_some_and(|left| {
matches!(
left.kind_id().into(),
Ruby::Constant
| Ruby::ScopeResolution
| Ruby::ScopeResolution2
| Ruby::ClassVariable
)
})
}
_ if RubyCode::is_call(node) => ruby_documented_call(node, code),
_ => false,
}
}
fn ruby_documented_call(node: &Node, code: &[u8]) -> bool {
let Some(method) = node.child_by_field_name("method") else {
return false;
};
match node_bytes(&method, code) {
b"attr" | b"attr_reader" | b"attr_writer" | b"attr_accessor" | b"alias_method" => true,
b"private"
| b"protected"
| b"public"
| b"module_function"
| b"private_class_method"
| b"public_class_method" => {
node.child_by_field_name("receiver").is_none()
&& node
.child_by_field_name("arguments")
.and_then(|arguments| arguments.children().find(Node::is_named))
.is_some_and(|argument| {
matches!(
argument.kind_id().into(),
Ruby::Method | Ruby::SingletonMethod
)
})
}
_ => false,
}
}
fn ruby_then_first_statement<'a>(node: &Node<'a>) -> Option<Node<'a>> {
(node.kind_id() == Ruby::Then)
.then(|| node.children().find(Node::is_named))
.flatten()
}
fn ruby_yard_reads<'a>(is_def: bool, scope: &Node<'a>, ancestors: Ancestors<'a, '_>) -> bool {
const DEF_READS: u8 = 0b01;
const NAMESPACED_READS: u8 = 0b10;
const BOTH: u8 = DEF_READS | NAMESPACED_READS;
let reads = ancestors.fold_down(
scope,
0,
FoldLane::DocScope,
BOTH,
|above, ancestor, _, _| match ancestor.kind_id().into() {
Ruby::Program => BOTH,
Ruby::Class
| Ruby::Module
| Ruby::SingletonClass
| Ruby::If
| Ruby::Unless
| Ruby::Elsif => {
if above & NAMESPACED_READS == 0 {
0
} else {
BOTH
}
}
Ruby::Method | Ruby::SingletonMethod => above & DEF_READS,
Ruby::Case | Ruby::CaseMatch | Ruby::Begin | Ruby::While | Ruby::Until | Ruby::For => 0,
_ => above,
},
);
let need = if is_def { DEF_READS } else { NAMESPACED_READS };
reads & need != 0
}
impl Checker for RubyCode {
fn is_comment(node: &Node) -> bool {
node.kind_id() == Ruby::Comment
}
fn is_doc_target<'a>(node: &Node<'a>, code: &[u8], ancestors: Ancestors<'a, '_>) -> bool {
let statement = ruby_then_first_statement(node).unwrap_or(*node);
ruby_documented_core(&statement).is_some_and(|core| {
ruby_documented_statement(&core, code)
&& ruby_yard_reads(
matches!(core.kind_id().into(), Ruby::Method | Ruby::SingletonMethod),
node,
ancestors,
)
})
}
fn doc_target_start_row(node: &Node) -> usize {
ruby_then_first_statement(node).unwrap_or(*node).start_row()
}
fn is_doc_run_directive(node: &Node, code: &[u8]) -> bool {
ruby_magic_comment(node_bytes(node, code))
}
fn is_doc_run_opener<'a>(
first: &Node<'a>,
_next: Option<&Node<'a>>,
code: &[u8],
ancestors: Ancestors<'a, '_>,
) -> bool {
first.kind_id() == Ruby::Comment
&& ruby_directive_opener(node_bytes(first, code))
&& ruby_yard_reads(false, first, ancestors)
}
fn is_func_space(node: &Node) -> bool {
matches!(
node.kind_id().into(),
Ruby::Program
| Ruby::Method
| Ruby::SingletonMethod
| Ruby::Lambda
| Ruby::Block
| Ruby::DoBlock
| Ruby::Class
| Ruby::SingletonClass
| Ruby::Module
)
}
fn is_func_space_with_code<'a>(
node: &Node<'a>,
_code: &[u8],
ancestors: Ancestors<'a, '_>,
) -> bool {
Self::is_func_space(node)
&& !(matches!(node.kind_id().into(), Ruby::Block | Ruby::DoBlock)
&& is_stabby_lambda_body(node, ancestors))
}
fn is_func<'a>(node: &Node<'a>, _ancestors: Ancestors<'a, '_>) -> bool {
matches!(node.kind_id().into(), Ruby::Method | Ruby::SingletonMethod)
}
fn is_closure<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> bool {
match node.kind_id().into() {
Ruby::Lambda => true,
Ruby::Block | Ruby::DoBlock => !is_stabby_lambda_body(node, ancestors),
_ => false,
}
}
fn is_call(node: &Node) -> bool {
matches!(
node.kind_id().into(),
Ruby::Call | Ruby::Call2 | Ruby::Call3 | Ruby::Call4
)
}
fn is_non_arg(node: &Node) -> bool {
matches!(
node.kind_id().into(),
Ruby::LPAREN
| Ruby::LPAREN2
| Ruby::RPAREN
| Ruby::RPAREN2
| Ruby::COMMA
| Ruby::SEMI
| Ruby::PIPE
)
}
impl_simple_is_string!(
Ruby,
String,
ChainedString,
BareString,
Subshell,
Regex,
HeredocBody,
DelimitedSymbol,
SimpleSymbol,
StringArray,
SymbolArray,
Character,
);
impl_is_else_if_clause!(Ruby, Elsif);
}