use bonsai_common::FileId;
use tree_sitter::Node;
use crate::ExpressionFlow;
use super::expression_flow::expression_flow_from_node_with_handler;
#[cfg(test)]
use super::GENERIC_HANDLER;
use super::{
first_identifier_descendant, first_identifier_like_child, looks_like_identifier, node_text,
GrammarHandler,
};
#[must_use]
#[cfg(test)]
pub fn extract_return_value_flow(node: &Node<'_>, file: FileId, src: &[u8]) -> ExpressionFlow {
extract_return_value_flow_with_handler(node, file, src, &GENERIC_HANDLER)
}
pub(super) fn extract_return_value_flow_with_handler(
node: &Node<'_>,
file: FileId,
src: &[u8],
handler: &GrammarHandler,
) -> ExpressionFlow {
let Some(value) = return_value_node(node) else {
return ExpressionFlow::default();
};
expression_flow_from_node_with_handler(value, file, src, handler)
}
pub(super) fn extract_return_value_kind_with_handler(
node: &Node<'_>,
src: &[u8],
handler: &GrammarHandler,
) -> Option<crate::AssignValueKind> {
return_value_node(node).and_then(|value| handler.expression_value_kind(value, src))
}
#[must_use]
#[cfg(test)]
pub fn extract_yield_value_flow(node: &Node<'_>, file: FileId, src: &[u8]) -> ExpressionFlow {
extract_yield_value_flow_with_handler(node, file, src, &GENERIC_HANDLER)
}
pub(super) fn extract_yield_value_flow_with_handler(
node: &Node<'_>,
file: FileId,
src: &[u8],
handler: &GrammarHandler,
) -> ExpressionFlow {
yield_value_node(node)
.map(|value| expression_flow_from_node_with_handler(value, file, src, handler))
.unwrap_or_default()
}
#[cfg(test)]
pub fn extract_return_value_name(node: &Node<'_>, src: &[u8]) -> Option<String> {
extract_return_value_name_with_handler(node, src, &GENERIC_HANDLER)
}
pub(super) fn extract_return_value_name_with_handler(
node: &Node<'_>,
src: &[u8],
handler: &GrammarHandler,
) -> Option<String> {
let Some(value_node) = return_value_node(node) else {
return None;
};
let value_kind = value_node.kind();
if looks_like_identifier(value_kind) {
if handler
.expression_call_span_extractor
.is_some_and(|extract| !extract(value_node).is_empty())
{
return None;
}
let text = node_text(&value_node, src).trim().to_string();
if !handler.is_literal_value(value_kind, &text) {
return Some(text);
}
return None;
}
if matches!(
value_kind,
"parenthesized_expression" | "expression_statement" | "single_expression"
) {
let unwrapped = first_identifier_like_child(&value_node)?;
return Some(node_text(&unwrapped, src).trim().to_string());
}
if matches!(value_kind, "scalar" | "array" | "hash") {
let mut cursor = value_node.walk();
let children: Vec<Node<'_>> = value_node.named_children(&mut cursor).collect();
if children.len() == 1 && children[0].kind() == "varname" {
let value = node_text(&value_node, src).trim();
if !value.is_empty() {
return Some(value.to_string());
}
}
}
if matches!(
value_kind,
"as_expression" | "satisfies_expression" | "non_null_expression" | "type_assertion"
) {
let inner = value_node
.child_by_field_name("expression")
.or_else(|| value_node.child_by_field_name("argument"))
.or_else(|| {
let mut cursor = value_node.walk();
let children: Vec<_> = value_node.named_children(&mut cursor).collect();
children.into_iter().find(|child| !child.kind().contains("type"))
})?;
if looks_like_identifier(inner.kind()) {
return Some(node_text(&inner, src).trim().to_string());
}
let operands = super::extract_rhs_expr_operands(&inner, src, handler);
if operands.len() == 1 {
return operands.into_iter().next();
}
return None;
}
let operands = super::extract_rhs_expr_operands(&value_node, src, handler);
if operands.len() == 1 {
return operands.into_iter().next();
}
None
}
fn catch_binding_identifier<'a>(node: Node<'a>) -> Option<Node<'a>> {
if matches!(node.kind(), "as_pattern" | "alias") {
if let Some(alias_node) = node.child_by_field_name("alias") {
return first_identifier_descendant(alias_node).or_else(|| {
if looks_like_identifier(alias_node.kind()) {
Some(alias_node)
} else {
None
}
});
}
}
if matches!(
node.kind(),
"as_pattern_target" | "catch_parameter" | "exception_parameter"
) {
return first_identifier_descendant(node).or_else(|| {
if looks_like_identifier(node.kind()) {
Some(node)
} else {
None
}
});
}
None
}
pub fn extract_return_value_text(node: &Node<'_>, src: &[u8]) -> Option<String> {
if let Some(text) = return_statement_value_text(node, src) {
return Some(text);
}
return_value_node(node).map(|n| node_text(&n, src).trim().to_string())
}
fn return_statement_value_text(node: &Node<'_>, src: &[u8]) -> Option<String> {
let raw_text = node_text(node, src).trim();
let after_keyword = raw_text.strip_prefix("return")?;
let value_text = after_keyword.trim_start();
if value_text.is_empty() {
return None;
}
let trimmed_value = value_text
.strip_suffix(';')
.unwrap_or(value_text)
.trim()
.to_string();
(!trimmed_value.is_empty()).then_some(trimmed_value)
}
fn return_value_node<'a>(node: &'a Node<'a>) -> Option<Node<'a>> {
if let Some(value_field) = node.child_by_field_name("value") {
return Some(value_field);
}
if let Some(expression_field) = node.child_by_field_name("expression") {
return Some(expression_field);
}
if let Some(argument_field) = node.child_by_field_name("argument") {
return Some(argument_field);
}
if let Some(body_field) = node.child_by_field_name("body") {
return Some(body_field);
}
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
if child.kind() != "return"
&& child.kind() != "return_keyword"
&& !child.kind().starts_with("comment")
{
return Some(child);
}
}
None
}
fn yield_value_node<'a>(node: &'a Node<'a>) -> Option<Node<'a>> {
for field in ["value", "expression", "argument", "operand", "body"] {
if let Some(value) = node.child_by_field_name(field) {
return Some(value);
}
}
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
if !matches!(child.kind(), "yield" | "yield_from" | "from" | "yield_keyword")
&& !child.kind().starts_with("comment")
{
return Some(child);
}
}
None
}
pub fn extract_throw_value_name(node: &Node<'_>, src: &[u8]) -> Option<String> {
let value_node = node
.child_by_field_name("value")
.or_else(|| node.child_by_field_name("expression"))
.or_else(|| node.child_by_field_name("exception"))
.or_else(|| node.child_by_field_name("cause"));
let value_node = value_node.or_else(|| {
let mut cursor = node.walk();
let named_children: Vec<_> = node.named_children(&mut cursor).collect();
named_children
.into_iter()
.find(|child| !matches!(child.kind(), "throw" | "raise" | "throws"))
})?;
if looks_like_identifier(value_node.kind()) {
Some(node_text(&value_node, src).trim().to_string())
} else {
None
}
}
pub fn extract_catch_param(try_node: &Node<'_>, src: &[u8]) -> Option<String> {
let mut cursor = try_node.walk();
for catch_arm in try_node.named_children(&mut cursor) {
let arm_kind = catch_arm.kind();
if !(arm_kind.contains("catch")
|| arm_kind.contains("except")
|| arm_kind.contains("rescue")
|| arm_kind == "on_part")
{
continue;
}
if let Some(parameter_node) = catch_arm
.child_by_field_name("parameter")
.or_else(|| catch_arm.child_by_field_name("catch_parameter"))
.or_else(|| catch_arm.child_by_field_name("exception_parameter"))
.or_else(|| catch_arm.child_by_field_name("name"))
.or_else(|| catch_arm.child_by_field_name("variable"))
{
let binding_identifier = catch_binding_identifier(parameter_node).or_else(|| {
first_identifier_descendant(parameter_node).or_else(|| {
if looks_like_identifier(parameter_node.kind()) {
Some(parameter_node)
} else {
None
}
})
});
if let Some(identifier) = binding_identifier {
return Some(node_text(&identifier, src).trim().to_string());
}
}
if let Some(value_node) = catch_arm.child_by_field_name("value") {
if let Some(identifier) = catch_binding_identifier(value_node) {
return Some(node_text(&identifier, src).trim().to_string());
}
}
let mut inner_cursor = catch_arm.walk();
for inner_child in catch_arm.named_children(&mut inner_cursor) {
let inner_kind = inner_child.kind();
if inner_kind == "as_pattern" || inner_kind == "as_pattern_target" || inner_kind == "alias" {
if let Some(identifier) =
catch_binding_identifier(inner_child).or_else(|| first_identifier_descendant(inner_child))
{
return Some(node_text(&identifier, src).trim().to_string());
}
}
}
if let Some(identifier) = first_identifier_descendant(catch_arm) {
return Some(node_text(&identifier, src).trim().to_string());
}
}
None
}