#![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, is_operand, last_operand, wrapped_operand,
};
use crate::metrics::cyclomatic::cpp_operator_is_applied;
use crate::*;
fn cpp_wrapper_operand<'a>(node: &Node<'a>) -> Option<(Node<'a>, bool)> {
let operator_is = |accept: &[&str]| {
node.child_by_field_name("operator")
.is_some_and(|op| accept.contains(&op.kind()))
};
let value = match node.kind() {
"parenthesized_expression" | "co_await_expression" => wrapped_operand(node),
"compound_statement" => last_operand(node)
.filter(|statement| statement.kind() == "expression_statement")
.and_then(|statement| wrapped_operand(&statement)),
"condition_clause" | "cast_expression" | "declaration" => node.child_by_field_name("value"),
"initializer_list" => sole_operand(node),
"comma_expression" => node.child_by_field_name("right"),
"assignment_expression" if operator_is(&["="]) => node.child_by_field_name("right"),
"unary_expression" if operator_is(&["!", "not"]) => {
return node.child_by_field_name("argument").map(|o| (o, true));
}
_ => None,
};
value.map(|o| (o, false))
}
fn sole_operand<'a>(list: &Node<'a>) -> Option<Node<'a>> {
let mut operands = list.children().filter(is_operand);
operands.next().filter(|_| operands.next().is_none())
}
const CPP_COMPARISONS: &[&str] = &["<", ">", "<=", ">=", "==", "!=", "not_eq", "<=>"];
fn cpp_condition_scores_itself(expr: &Node) -> bool {
let operator_in = |accept: &[&str]| {
expr.child_by_field_name("operator")
.is_some_and(|op| accept.contains(&op.kind()))
};
match expr.kind() {
"conditional_expression" => true,
"binary_expression" | "fold_expression" => {
operator_in(CPP_COMPARISONS) || operator_in(&["&&", "||", "and", "or"])
}
_ => false,
}
}
fn cpp_count_condition(condition: &Node, conditions: &mut f64) {
count_boolean_slot(
condition,
cpp_wrapper_operand,
cpp_condition_scores_itself,
conditions,
);
}
fn cpp_count_negated(operand: &Node, conditions: &mut f64) {
count_negated_operand(
operand,
cpp_wrapper_operand,
cpp_condition_scores_itself,
conditions,
);
}
pub(super) fn cpp_count_condition_slot(statement: &Node, conditions: &mut f64) {
if let Some(condition) = statement.child_by_field_name("condition") {
cpp_count_condition(&condition, conditions);
}
}
pub(super) fn cpp_count_return(statement: &Node, conditions: &mut f64) {
if let Some(value) = wrapped_operand(statement) {
cpp_count_negated(&value, conditions);
}
}
pub(super) fn cpp_count_arguments(list: &Node, conditions: &mut f64) {
count_each_operand(list, cpp_count_negated, conditions);
}
pub(super) fn cpp_count_chain_operands(chain: &Node, conditions: &mut f64) {
match chain.kind() {
"binary_expression" => count_field_operands(chain, cpp_count_condition, conditions),
"fold_expression" => {
let mut written = 0;
for operand in chain.children().filter(is_operand) {
cpp_count_condition(&operand, conditions);
written += 1;
}
if written == 1 {
*conditions += 1.;
}
}
_ => {}
}
}
pub(super) fn cpp_walk_ternary(node: &Node, conditions: &mut f64) {
if let Some(condition) = node.child_by_field_name("condition") {
cpp_count_condition(&condition, conditions);
}
for field in ["consequence", "alternative"] {
if let Some(branch) = node.child_by_field_name(field) {
cpp_count_negated(&branch, conditions);
}
}
}
pub(super) fn cpp_walk_for_statement(node: &Node, conditions: &mut f64) {
if let Some(condition) = node.child_by_field_name("condition") {
cpp_count_condition(&condition, conditions);
}
}
impl Abc for CppCode {
fn compute<'a>(
node: &Node<'a>,
_code: &'a [u8],
ancestors: Ancestors<'a, '_>,
stats: &mut Stats,
) {
use Cpp::*;
let opens = matches!(node.kind_id().into(), RequiresClause | RequiresExpression);
let in_constraint = stats.constraint.covers(node, opens);
match node.kind_id().into() {
AssignmentExpression | AssignmentExpression2 | UpdateExpression => {
stats.assignments += 1.;
}
InitDeclarator if node.first_child(|id| id == EQ as u16).is_some() => {
stats.assignments += 1.;
}
CallExpression | CallExpression2 | NewExpression => {
stats.branches += 1.;
}
_ if in_constraint => {}
Else | Case | QMARK | Try | Try2 | Catch => {
stats.conditions += 1.;
}
LT | GT | LTEQ | GTEQ | EQEQ | BANGEQ | NotEq | LTEQGT
if cpp_operator_is_applied(node, ancestors) =>
{
stats.conditions += 1.;
}
AMPAMP | PIPEPIPE | And | Or if cpp_operator_is_applied(node, ancestors) => {
if let Some(parent) = ancestors.parent(node) {
cpp_count_chain_operands(&parent, &mut stats.conditions);
}
}
IfStatement | WhileStatement | DoStatement => {
cpp_count_condition_slot(node, &mut stats.conditions);
}
ReturnStatement | CoReturnStatement => {
cpp_count_return(node, &mut stats.conditions);
}
ArgumentList | ArgumentList2 => {
cpp_count_arguments(node, &mut stats.conditions);
}
ConditionalExpression => {
cpp_walk_ternary(node, &mut stats.conditions);
}
ForStatement => {
cpp_walk_for_statement(node, &mut stats.conditions);
}
_ => {}
}
}
}
#[cfg(test)]
#[allow(clippy::float_cmp)]
mod tests {
use super::{cpp_count_chain_operands, cpp_count_condition_slot, cpp_count_negated};
use crate::traits::ParserTrait;
use crate::{CppParser, Node};
#[cfg(feature = "cpp")]
fn parse(src: &str) -> CppParser {
CppParser::new(
src.as_bytes().to_vec(),
std::path::Path::new("seam.cpp"),
None,
)
}
#[cfg(feature = "cpp")]
fn first_of_kind<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
let mut stack = vec![node];
while let Some(n) = stack.pop() {
if n.kind() == kind {
return Some(n);
}
for i in (0..n.child_count()).rev() {
if let Some(c) = n.child(i) {
stack.push(c);
}
}
}
None
}
#[cfg(feature = "cpp")]
#[test]
fn chain_operands_each_pay_one_slot() {
for src in [
"int f(int a, int b) { return a && b; }",
"int f(int a, int b) { return a && -b; }",
] {
let p = parse(src);
let bin = first_of_kind(p.root(), "binary_expression")
.expect("`a && b` parses to a binary_expression");
let mut conditions = 0.;
cpp_count_chain_operands(&bin, &mut conditions);
assert_eq!(conditions, 2., "{src}");
}
}
#[cfg(feature = "cpp")]
#[test]
fn condition_slot_pays_once_unless_counted() {
for (src, expected) in [
("void f(int a) { if (a) {} }", 1.),
("void f(int a) { if (((a))) {} }", 1.),
("void f(int a) { if (!a) {} }", 1.),
("void f(int a) { if (-a) {} }", 1.),
("void f(int a) { if (a > 1) {} }", 0.),
] {
let p = parse(src);
let statement =
first_of_kind(p.root(), "if_statement").expect("the fixture holds an `if`");
let mut conditions = 0.;
cpp_count_condition_slot(&statement, &mut conditions);
assert_eq!(conditions, expected, "{src}");
}
}
#[cfg(feature = "cpp")]
#[test]
fn negated_operand_counts_only_behind_a_negation() {
for (src, kind, expected) in [
(
"int g(int a) { int x = (a); return x; }",
"parenthesized_expression",
0.,
),
(
"int g(int a) { int x = !a; return x; }",
"unary_expression",
1.,
),
] {
let p = parse(src);
let operand = first_of_kind(p.root(), kind).expect("the fixture holds the operand");
let mut conditions = 0.;
cpp_count_negated(&operand, &mut conditions);
assert_eq!(conditions, expected, "{src}");
}
}
}