#![allow(
clippy::enum_glob_use,
clippy::too_many_lines,
clippy::wildcard_imports
)]
#![allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
use super::{
Abc, Stats, count_boolean_slot, count_each_operand, count_negated_operand, is_operand, peel,
wrapped_operand,
};
use crate::lang_helpers::perl::{
perl_and_is_operator, perl_bareword_is_call, perl_dash_signs_keyword_key, perl_not_is_key,
};
use crate::*;
fn perl_is_operand(child: &Node) -> bool {
is_operand(child) && !matches!(child.kind_id().into(), Perl::NormalComma | Perl::FatComma)
}
fn perl_last_operand<'a>(node: &Node<'a>) -> Option<Node<'a>> {
node.children().filter(perl_is_operand).last()
}
fn perl_unparenthesized(node: Node<'_>) -> Node<'_> {
let mut node = node;
while node.kind_id() == Perl::Array as u16
&& let Some(inner) = perl_last_operand(&node)
{
node = inner;
}
node
}
fn perl_wrapper_operand<'a>(node: &Node<'a>) -> Option<(Node<'a>, bool)> {
use Perl as P;
match node.kind_id().into() {
P::Array | P::Arguments => perl_last_operand(node).map(|o| (o, false)),
P::ParenthesizedArgument => wrapped_operand(node).map(|o| (o, false)),
P::BinaryExpression if node.is_child(P::EQ as u16) => {
perl_last_operand(node).map(|o| (o, false))
}
P::UnaryExpression => {
node.child_by_field_name("operator")
.and_then(|op| match op.kind_id().into() {
P::Not if perl_not_is_key(&op, node) => {
perl_last_operand(node).map(|o| (o, false))
}
P::And if !perl_and_is_operator(&op, node) => {
perl_last_operand(node).map(|o| (o, false))
}
P::DASH
if perl_dash_signs_keyword_key(
&op,
Ancestors::known(std::slice::from_ref(node)),
) =>
{
wrapped_operand(node).map(|o| (o, false))
}
P::BANG | P::Not => wrapped_operand(node).map(|o| (o, true)),
_ => None,
})
}
_ => None,
}
}
fn perl_condition_scores_itself(expr: &Node) -> bool {
use Perl as P;
match expr.kind_id().into() {
P::TernaryExpression | P::PatternMatcher | P::PatternMatcherM => true,
P::BinaryExpression | P::UnaryExpression => {
perl_is_logical_chain(expr)
|| expr.children().any(|token| match token.kind_id().into() {
P::EQTILDE | P::BANGTILDE => !perl_binding_rewrites(expr),
kind => matches!(
kind,
P::EQEQ
| P::BANGEQ
| P::LT
| P::GT
| P::LTEQ
| P::GTEQ
| P::LTEQGT
| P::Eq
| P::Ne
| P::Lt
| P::Gt
| P::Le
| P::Ge
| P::Cmp
),
})
}
_ => false,
}
}
fn perl_binding_rewrites(binding: &Node) -> bool {
binding.kind_id() == Perl::BinaryExpression as u16
&& perl_last_operand(binding).is_some_and(|rhs| {
matches!(
perl_unparenthesized(rhs).kind_id().into(),
Perl::SubstitutionPatternS | Perl::TransliterationTrOrY
)
})
}
fn perl_count_condition(condition: &Node, conditions: &mut f64) {
count_boolean_slot(
condition,
perl_wrapper_operand,
perl_condition_scores_itself,
conditions,
);
}
fn perl_count_negated(operand: &Node, conditions: &mut f64) {
count_negated_operand(
operand,
perl_wrapper_operand,
perl_condition_scores_itself,
conditions,
);
}
fn perl_count_slot(slot: Option<Node>, conditions: &mut f64) {
if let Some(condition) = slot {
perl_count_condition(&condition, conditions);
}
}
fn perl_misparse_absorbs_leading_operand<'a>(
node: &Node<'a>,
ancestors: Ancestors<'a, '_>,
) -> bool {
use Perl as P;
if perl_leading_slot(node)
.is_none_or(|lead| perl_condition_scores_itself(&peel(&lead, perl_wrapper_operand).0))
{
return false;
}
let (mut child, mut child_ancestors) = (*node, ancestors);
let spine_parent = ancestors.iter(node).find(|(parent, above)| {
let on_spine = perl_leading_slot(parent).is_some_and(|lead| lead.id() == child.id());
if on_spine {
(child, child_ancestors) = (*parent, *above);
}
!on_spine
});
spine_parent.is_some_and(|(parent, _)| {
(parent.kind_id() == P::BinaryExpression as u16 || parent.is_error())
&& child_ancestors
.siblings_before(&child)
.find(|sibling| !sibling.as_tree_sitter().is_extra())
.is_some_and(|operator| {
matches!(
operator.kind_id().into(),
P::LT
| P::GT
| P::LTEQ
| P::GTEQ
| P::Lt
| P::Gt
| P::Le
| P::Ge
| P::EQEQ
| P::BANGEQ
| P::Eq
| P::Ne
)
})
})
}
fn perl_leading_slot<'a>(node: &Node<'a>) -> Option<Node<'a>> {
if node.kind_id() == Perl::TernaryExpression as u16 {
node.child_by_field_name("condition")
} else if perl_is_logical_chain(node) {
wrapped_operand(node)
} else {
None
}
}
fn perl_is_logical_chain(node: &Node) -> bool {
use Perl as P;
matches!(
node.kind_id().into(),
P::BinaryExpression | P::UnaryExpression
) && node.children().any(|token| match token.kind_id().into() {
P::And => perl_and_is_operator(&token, node),
P::AMPAMP | P::PIPEPIPE | P::SLASHSLASH | P::Or => true,
_ => false,
})
}
fn perl_walk_ternary(node: &Node, condition_absorbed: bool, conditions: &mut f64) {
if !condition_absorbed {
perl_count_slot(node.child_by_field_name("condition"), conditions);
}
for field in ["true", "false"] {
if let Some(branch) = node.child_by_field_name(field) {
perl_count_negated(&branch, conditions);
}
}
}
fn perl_count_chain_operands(chain: &Node, leading_absorbed: bool, conditions: &mut f64) {
if perl_is_logical_chain(chain) {
for operand in chain
.children()
.filter(perl_is_operand)
.skip(usize::from(leading_absorbed))
{
perl_count_condition(&operand, conditions);
}
}
}
fn perl_pattern_is_bound_or_delimiter(pattern: &Node, code: &[u8], ancestors: Ancestors) -> bool {
use Perl as P;
let mut up = ancestors.iter(pattern).map(|(ancestor, _)| ancestor);
let mut slot = *pattern;
let mut owner = up.next();
let mut above = up.next();
while let Some(paren) = owner
&& paren.kind_id() == P::Array as u16
&& !paren
.children_after(&slot)
.any(|next| perl_is_operand(&next))
&& !above.is_some_and(perl_is_call_argument_parent)
{
slot = paren;
owner = above;
above = up.next();
}
owner.is_some_and(|owner| match owner.kind_id().into() {
P::BinaryExpression => {
owner
.child_by_field_name("operator")
.is_some_and(|op| matches!(op.kind_id().into(), P::EQTILDE | P::BANGTILDE))
&& perl_last_operand(&owner).is_some_and(|rhs| rhs.id() == slot.id())
}
P::Arguments | P::Array => {
owner
.children()
.find(perl_is_operand)
.is_some_and(|first| first.id() == slot.id())
&& above.is_some_and(|call| perl_call_is_split(&call, code))
}
_ => false,
})
}
fn perl_call_is_split(call: &Node, code: &[u8]) -> bool {
let text = |node: &Node| code.get(node.start_byte()..node.end_byte());
matches!(
call.kind_id().into(),
Perl::CallExpressionWithSpacedArgs | Perl::CallExpressionWithArgsWithBrackets
) && call
.children()
.find(|child| child.kind_id() == Perl::CallExpressionWithBareword as u16)
.is_some_and(|callee| {
callee
.child_by_field_name("function_name")
.is_some_and(|name| text(&name) == Some(b"split"))
&& callee
.children()
.find(|child| child.kind_id() == Perl::PackageName as u16)
.is_none_or(|package| text(&package) == Some(b"CORE"))
})
}
fn perl_is_call_argument_parent(parent: Node) -> bool {
use Perl as P;
matches!(
parent.kind_id().into(),
P::CallExpressionWithArgsWithBrackets
| P::CallExpressionWithSpacedArgs
| P::CallExpressionWithSub
| P::CallExpressionWithVariable
| P::CallExpressionRecursive
| P::MethodInvocation
)
}
impl Abc for PerlCode {
fn compute<'a>(
node: &Node<'a>,
code: &'a [u8],
ancestors: Ancestors<'a, '_>,
stats: &mut Stats,
) {
use Perl as P;
match node.kind_id().into() {
P::EQ
| P::PLUSEQ
| P::DASHEQ
| P::STAREQ
| P::SLASHEQ
| P::PERCENTEQ
| P::STARSTAREQ
| P::DOTEQ
| P::XEQ
| P::AMPEQ
| P::PIPEEQ
| P::CARETEQ
| P::LTLTEQ
| P::GTGTEQ
| P::AMPAMPEQ
| P::PIPEPIPEEQ
| P::SLASHSLASHEQ
| P::AMPDOTEQ
| P::PIPEDOTEQ
| P::CARETDOTEQ => {
stats.assignments += 1.;
}
P::CallExpressionWithSpacedArgs
| P::CallExpressionWithSub
| P::CallExpressionWithArgsWithBrackets
| P::CallExpressionWithVariable
| P::CallExpressionRecursive
| P::MethodInvocation => {
stats.branches += 1.;
}
P::CallExpressionWithBareword if perl_bareword_is_call(node, ancestors) => {
stats.branches += 1.;
}
P::EQEQ
| P::BANGEQ
| P::LTEQ
| P::GTEQ
| P::LTEQGT
| P::Eq
| P::Ne
| P::Lt
| P::Gt
| P::Le
| P::Ge
| P::Cmp
| P::ElseClause => {
stats.conditions += 1.;
}
P::EQTILDE | P::BANGTILDE
if !ancestors
.parent(node)
.is_some_and(|binding| perl_binding_rewrites(&binding)) =>
{
stats.conditions += 1.;
}
P::LT | P::GT if ancestors.parent_has_kind(node, P::BinaryExpression as u16) => {
stats.conditions += 1.;
}
P::PatternMatcher | P::PatternMatcherM
if !perl_pattern_is_bound_or_delimiter(node, code, ancestors) =>
{
stats.conditions += 1.;
}
P::AMPAMP | P::PIPEPIPE | P::SLASHSLASH | P::And | P::Or => {
if let Some((chain, above)) = ancestors.iter(node).next() {
let absorbed = perl_misparse_absorbs_leading_operand(&chain, above);
perl_count_chain_operands(&chain, absorbed, &mut stats.conditions);
}
}
P::ElsifClause => {
stats.conditions += 1.;
perl_count_slot(node.child_by_field_name("condition"), &mut stats.conditions);
}
P::ReturnExpression => {
if let Some(value) = wrapped_operand(node) {
perl_count_negated(&value, &mut stats.conditions);
}
}
P::Array
if ancestors
.parent(node)
.is_some_and(perl_is_call_argument_parent) =>
{
count_each_operand(node, perl_count_negated, &mut stats.conditions);
}
P::TernaryExpression => {
stats.conditions += 1.;
let absorbed = perl_misparse_absorbs_leading_operand(node, ancestors);
perl_walk_ternary(node, absorbed, &mut stats.conditions);
}
P::IfStatement
| P::UnlessStatement
| P::WhileStatement
| P::UntilStatement
| P::ForStatement1
| P::IfSimpleStatement
| P::UnlessSimpleStatement
| P::WhileSimpleStatement
| P::UntilSimpleStatement
| P::WhenSimpleStatement => {
perl_count_slot(node.child_by_field_name("condition"), &mut stats.conditions);
}
_ => {}
}
}
}