#![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_field_operands,
count_negated_operand, last_operand, peel, wrapped_operand,
};
use crate::macros::{csharp_paren_expr_kinds, csharp_prefix_unary_expr_kinds};
use crate::*;
fn csharp_wrapper_operand<'a>(node: &Node<'a>) -> Option<(Node<'a>, bool)> {
use Csharp::*;
match node.kind_id().into() {
csharp_paren_expr_kinds!() => wrapped_operand(node).map(|o| (o, false)),
CheckedExpression | AwaitExpression => wrapped_operand(node).map(|o| (o, false)),
csharp_prefix_unary_expr_kinds!()
if node.child(0).is_some_and(|c| c.kind_id() == BANG as u16) =>
{
wrapped_operand(node).map(|o| (o, true))
}
PostfixUnaryExpression if node.is_child(BANG as u16) => Some((node.child(0)?, false)),
CastExpression => node.child_by_field_name("value").map(|o| (o, false)),
_ => None,
}
}
fn csharp_condition_scores_itself(expr: &Node) -> bool {
use Csharp::*;
match expr.kind_id().into() {
ConditionalExpression | IsExpression | IsPatternExpression => true,
BinaryExpression | BinaryExpression2 => {
expr.child_by_field_name("operator").is_some_and(|op| {
matches!(
op.kind_id().into(),
EQEQ | BANGEQ | LT | GT | LTEQ | GTEQ | QMARKQMARK | AMPAMP | PIPEPIPE
)
})
}
AssignmentExpression => expr.child_by_field_name("right").is_some_and(|right| {
let (value, proves_boolean) = peel(&right, csharp_wrapper_operand);
proves_boolean || csharp_condition_scores_itself(&value)
}),
_ => false,
}
}
fn csharp_count_condition(condition: &Node, conditions: &mut f64) {
count_boolean_slot(
condition,
csharp_wrapper_operand,
csharp_condition_scores_itself,
conditions,
);
}
fn csharp_count_negated(operand: &Node, conditions: &mut f64) {
count_negated_operand(
operand,
csharp_wrapper_operand,
csharp_condition_scores_itself,
conditions,
);
}
fn csharp_count_negated_slot(slot: Option<Node>, conditions: &mut f64) {
if let Some(operand) = slot {
csharp_count_negated(&operand, conditions);
}
}
fn csharp_count_arguments(arguments: &Node, conditions: &mut f64) {
count_each_operand(arguments, csharp_count_argument, conditions);
}
fn csharp_count_argument(argument: &Node, conditions: &mut f64) {
if let Some(value) = last_operand(argument) {
csharp_count_negated(&value, conditions);
}
}
fn csharp_eq_initializes_const_binding<'a>(
eq_node: &Node<'a>,
ancestors: Ancestors<'a, '_>,
) -> bool {
use Csharp::*;
let mut climb = ancestors.iter(eq_node).map(|(ancestor, _)| ancestor);
let is_declaration = |node: &Node| {
matches!(
node.kind_id().into(),
VariableDeclaration | VariableDeclaration2
)
};
if !climb.next().is_some_and(|declarator| {
matches!(
declarator.kind_id().into(),
VariableDeclarator | VariableDeclarator2
)
}) {
return false;
}
if !climb
.next()
.is_some_and(|declaration| is_declaration(&declaration))
{
return false;
}
climb.next().is_some_and(|statement| {
matches!(
statement.kind_id().into(),
LocalDeclarationStatement | FieldDeclaration
) && statement
.children()
.take_while(|child| !is_declaration(child))
.any(|child| child.kind_id() == Modifier && child.is_child(Const as u16))
})
}
fn csharp_count_token_assignment<'a>(
node: &Node<'a>,
ancestors: Ancestors<'a, '_>,
stats: &mut Stats,
) -> bool {
use Csharp::*;
match node.kind_id().into() {
STAREQ | SLASHEQ | PERCENTEQ | DASHEQ | PLUSEQ | LTLTEQ | GTGTEQ | GTGTGTEQ | AMPEQ
| PIPEEQ | CARETEQ | QMARKQMARKEQ | PLUSPLUS | DASHDASH => {
stats.assignments += 1.;
}
EQ => {
if !csharp_eq_initializes_const_binding(node, ancestors) {
stats.assignments += 1.;
}
}
_ => return false,
}
true
}
fn csharp_count_token_branch<'a>(
node: &Node<'a>,
ancestors: Ancestors<'a, '_>,
stats: &mut Stats,
) -> bool {
use Csharp::*;
let is_branch = match node.kind_id().into() {
crate::Csharp::InvocationExpression
| crate::Csharp::InvocationExpression2
| crate::Csharp::InvocationExpression3
| ObjectCreationExpression
| ConstructorInitializer => true,
PrimaryConstructorBaseType | ArgumentList => ancestors
.parent(node)
.is_some_and(|parent| matches!(parent.kind_id().into(), BaseList | BaseList2)),
_ => false,
};
if is_branch {
stats.branches += 1.;
}
is_branch
}
fn csharp_count_token_condition<'a>(
node: &Node<'a>,
ancestors: Ancestors<'a, '_>,
stats: &mut Stats,
) -> bool {
use Csharp::*;
match node.kind_id().into() {
Else | Try | Catch | QMARKQMARK | IsExpression | IsPatternExpression => {
stats.conditions += 1.;
}
Case if crate::metrics::cyclomatic::csharp_case_token_is_switch_arm(node, ancestors) => {
stats.conditions += 1.;
}
GT | LT | GTEQ | LTEQ | EQEQ | BANGEQ
if ancestors.parent(node).is_some_and(|parent| {
matches!(
parent.kind_id().into(),
BinaryExpression | BinaryExpression2
)
}) =>
{
stats.conditions += 1.;
}
QMARK
if ancestors.parent(node).is_some_and(|parent| {
!matches!(
parent.kind_id().into(),
NullableType | TypeParameterConstraint
)
}) =>
{
stats.conditions += 1.;
}
SwitchExpressionArm
if !crate::metrics::cyclomatic::csharp_switch_expression_arm_is_bare_discard(node) =>
{
stats.conditions += 1.;
}
_ => return false,
}
true
}
fn csharp_walk_for_conditions<'a>(
node: &Node<'a>,
ancestors: Ancestors<'a, '_>,
stats: &mut Stats,
) {
use Csharp::*;
let conds = &mut stats.conditions;
match node.kind_id().into() {
AMPAMP | PIPEPIPE => {
if let Some(chain) = ancestors
.parent(node)
.filter(|p| matches!(p.kind_id().into(), BinaryExpression | BinaryExpression2))
{
count_field_operands(&chain, csharp_count_condition, conds);
}
}
ArgumentList => csharp_count_arguments(node, conds),
IfStatement | WhileStatement | DoStatement => {
if let Some(condition) = node.child_by_field_name("condition") {
csharp_count_condition(&condition, conds);
}
}
WhenClause | CatchFilterClause => {
if let Some(guard) = wrapped_operand(node) {
csharp_count_condition(&guard, conds);
}
}
ReturnStatement => csharp_count_negated_slot(wrapped_operand(node), conds),
crate::Csharp::VariableDeclarator | crate::Csharp::VariableDeclarator2 => {
csharp_count_negated_slot(last_operand(node), conds);
}
AssignmentExpression => csharp_count_negated_slot(node.child_by_field_name("right"), conds),
LambdaExpression => csharp_count_negated_slot(node.child_by_field_name("body"), conds),
ConditionalExpression => csharp_walk_conditional(node, stats),
ForStatement => csharp_walk_for_statement(node, stats),
_ => {}
}
}
fn csharp_walk_conditional(node: &Node, stats: &mut Stats) {
let conds = &mut stats.conditions;
if let Some(condition) = node.child_by_field_name("condition") {
csharp_count_condition(&condition, conds);
}
for field in ["consequence", "alternative"] {
csharp_count_negated_slot(node.child_by_field_name(field), conds);
}
}
fn csharp_walk_for_statement(node: &Node, stats: &mut Stats) {
if let Some(condition) = node.child_by_field_name("condition") {
csharp_count_condition(&condition, &mut stats.conditions);
}
}
impl Abc for CsharpCode {
fn compute<'a>(
node: &Node<'a>,
_code: &'a [u8],
ancestors: Ancestors<'a, '_>,
stats: &mut Stats,
) {
if csharp_count_token_assignment(node, ancestors, stats) {
return;
}
if csharp_count_token_branch(node, ancestors, stats) {
if node.kind_id() == Csharp::ArgumentList {
csharp_count_arguments(node, &mut stats.conditions);
}
return;
}
if csharp_count_token_condition(node, ancestors, stats) {
return;
}
csharp_walk_for_conditions(node, ancestors, stats);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::for_each_node_with_chain;
#[cfg(feature = "csharp")]
#[test]
fn const_predicate_fails_closed_on_a_truncated_chain() {
let source = b"class A { const int x = 1; }";
let mut seen = 0;
for_each_node_with_chain::<CsharpCode>(source, |node, chain| {
if node.kind() != "=" {
return;
}
let Some(declarator) = chain.last() else {
return;
};
if declarator.kind_id() != Csharp::VariableDeclarator as u16 {
return;
}
seen += 1;
assert!(
csharp_eq_initializes_const_binding(node, Ancestors::known(chain)),
"the full chain reaches the `const` declaration"
);
let declarator_only = &chain[chain.len() - 1..];
assert!(
!csharp_eq_initializes_const_binding(node, Ancestors::known(declarator_only)),
"a chain that ends at the declarator must not read as `const`"
);
assert!(
!csharp_eq_initializes_const_binding(node, Ancestors::known(&[])),
"an empty chain must not read as `const`"
);
});
assert_eq!(seen, 1, "fixture must carry exactly one declarator `=`");
}
}