use crate::expr::{ExprRef, JsExpr};
use oxc_ast::ast::{ConditionalExpression, Expression, LogicalExpression, LogicalOperator};
use oxc_ast_visit::{Visit, walk};
use vize_l0::Span;
use vize_l0::id::NodeId;
use super::{ComplexityFacts, Contribution, DecisionKind};
#[cfg(test)]
mod tests;
#[inline]
pub(super) fn score(
expr: ExprRef<'_>,
op: Option<NodeId>,
depth: u32,
facts: &mut ComplexityFacts,
) {
match expr {
ExprRef::Js(js) if terminal(js.ast) => {}
ExprRef::Js(js) => score_js(js, op, depth, facts),
ExprRef::Opaque(_) | ExprRef::Foreign(_) | ExprRef::Filter(_) => {
unknown(expr, op, depth, facts);
}
}
}
fn score_js<'a>(js: &'a JsExpr<'a>, op: Option<NodeId>, depth: u32, facts: &mut ComplexityFacts) {
let previous = (facts.contributions.len(), facts.cyclomatic, facts.cognitive);
let mut scorer = Scorer {
js,
op,
nesting: depth,
facts,
unprojectable: false,
};
scorer.visit_expression(js.ast);
if scorer.unprojectable {
facts.contributions.truncate(previous.0);
facts.cyclomatic = previous.1;
facts.cognitive = previous.2;
unknown(ExprRef::Js(js), op, depth, facts);
}
}
fn terminal(expr: &Expression<'_>) -> bool {
matches!(
expr,
Expression::Identifier(_)
| Expression::BooleanLiteral(_)
| Expression::NullLiteral(_)
| Expression::NumericLiteral(_)
| Expression::BigIntLiteral(_)
| Expression::RegExpLiteral(_)
| Expression::StringLiteral(_)
| Expression::ThisExpression(_)
| Expression::Super(_)
)
}
fn unknown(expr: ExprRef<'_>, op: Option<NodeId>, depth: u32, facts: &mut ComplexityFacts) {
facts.push(Contribution {
span: expr.span(),
kind: DecisionKind::Unknown,
op,
nesting: depth,
cyclomatic: 0,
cognitive: 0,
});
}
struct Scorer<'f, 'a> {
js: &'a JsExpr<'a>,
op: Option<NodeId>,
nesting: u32,
facts: &'f mut ComplexityFacts,
unprojectable: bool,
}
struct Run {
operators: u32,
runs: u32,
last: Option<LogicalOperator>,
}
impl<'a> Scorer<'_, 'a> {
fn file_span(&mut self, span: oxc_span::Span) -> Option<Span> {
let decoded = if self.js.coordinates.is_none() {
Some(Span::new(span.start, span.end))
} else {
self.js.ast_span_to_source(span)
};
let authored = decoded.and_then(|decoded| self.js.authored_span(decoded));
self.unprojectable |= authored.is_none();
authored
}
fn flatten(&mut self, logical: &LogicalExpression<'a>, run: &mut Run) {
self.operand(&logical.left, run);
run.operators = run.operators.saturating_add(1);
let operator = Some(logical.operator);
if run.last != operator {
run.runs = run.runs.saturating_add(1);
run.last = operator;
}
self.operand(&logical.right, run);
}
fn operand(&mut self, operand: &Expression<'a>, run: &mut Run) {
match operand.without_parentheses() {
Expression::LogicalExpression(inner) => {
vize_l0::ensure_sufficient_stack(|| self.flatten(inner, run));
}
_ => self.visit_expression(operand),
}
}
}
impl<'a> Visit<'a> for Scorer<'_, 'a> {
fn visit_expression(&mut self, it: &Expression<'a>) {
if !terminal(it) {
walk::walk_expression(self, it);
}
}
fn visit_logical_expression(&mut self, it: &LogicalExpression<'a>) {
let Some(span) = self.file_span(it.span) else {
return;
};
let nesting = self.nesting;
let mut run = Run {
operators: 0,
runs: 0,
last: None,
};
self.flatten(it, &mut run);
self.facts.push(Contribution {
span,
kind: DecisionKind::Logical,
op: self.op,
nesting,
cyclomatic: run.operators,
cognitive: run.runs,
});
}
fn visit_conditional_expression(&mut self, it: &ConditionalExpression<'a>) {
let Some(span) = self.file_span(it.span) else {
return;
};
let nesting = self.nesting;
self.facts.push(Contribution {
span,
kind: DecisionKind::Conditional,
op: self.op,
nesting,
cyclomatic: 1,
cognitive: 1 + nesting,
});
self.nesting = nesting.saturating_add(1);
vize_l0::ensure_sufficient_stack(|| walk::walk_conditional_expression(self, it));
self.nesting = nesting;
}
}