use super::*;
#[inline]
fn compute_halstead_mi_and_wmc<T: ParserTrait>(state: &mut State, selected: MetricSet) {
if selected.contains(Metric::Halstead) {
state
.halstead_maps
.finalize(&mut state.space.metrics.halstead);
}
if selected.contains(Metric::Mi) {
T::Mi::compute(
&state.space.metrics.loc,
&state.space.metrics.cyclomatic,
&state.space.metrics.halstead,
&mut state.space.metrics.mi,
);
}
if selected.contains(Metric::Wmc) {
T::Wmc::compute(
state.space.kind,
&state.space.metrics.cyclomatic,
&mut state.space.metrics.wmc,
);
}
}
#[inline]
fn compute_averages(state: &mut State, selected: MetricSet) {
let nom_functions = state.space.metrics.nom.functions_sum() as usize;
let nom_closures = state.space.metrics.nom.closures_sum() as usize;
let nom_total = state.space.metrics.nom.total() as usize;
if selected.contains(Metric::Cognitive) {
state.space.metrics.cognitive.finalize(nom_total);
}
if selected.contains(Metric::Nexits) {
state.space.metrics.nexits.finalize(nom_total);
}
if selected.contains(Metric::Nargs) {
state
.space
.metrics
.nargs
.finalize(nom_functions, nom_closures);
}
}
#[inline]
fn compute_minmax(state: &mut State, selected: MetricSet) {
if selected.contains(Metric::Cyclomatic) {
state.space.metrics.cyclomatic.compute_minmax();
}
if selected.contains(Metric::Nexits) {
state.space.metrics.nexits.compute_minmax();
}
if selected.contains(Metric::Cognitive) {
state.space.metrics.cognitive.compute_minmax();
}
if selected.contains(Metric::Nargs) {
state.space.metrics.nargs.compute_minmax();
}
if selected.contains(Metric::Nom) {
state.space.metrics.nom.compute_minmax();
}
if selected.contains(Metric::Loc) {
state.space.metrics.loc.compute_minmax();
}
if selected.contains(Metric::Abc) {
state.space.metrics.abc.compute_minmax();
}
if selected.contains(Metric::Tokens) {
state.space.metrics.tokens.compute_minmax();
}
}
#[inline]
fn compute_sum(state: &mut State, selected: MetricSet) {
if selected.contains(Metric::Wmc) {
state.space.metrics.wmc.compute_sum();
}
if selected.contains(Metric::Npm) {
state.space.metrics.npm.compute_sum();
}
if selected.contains(Metric::Npa) {
state.space.metrics.npa.compute_sum();
}
}
fn finalize_state<T: ParserTrait>(state: &mut State, selected: MetricSet) {
compute_minmax(state, selected);
compute_sum(state, selected);
compute_halstead_mi_and_wmc::<T>(state, selected);
compute_averages(state, selected);
}
fn finalize<T: ParserTrait>(state_stack: &mut Vec<State>, diff_level: usize, selected: MetricSet) {
if state_stack.is_empty() {
return;
}
for _ in 0..diff_level {
if state_stack.len() == 1 {
let last_state = state_stack
.last_mut()
.expect("invariant: state_stack has exactly one element");
finalize_state::<T>(last_state, selected);
break;
}
let mut state = state_stack
.pop()
.expect("invariant: state_stack has more than one element");
finalize_state::<T>(&mut state, selected);
let last_state = state_stack
.last_mut()
.expect("invariant: state_stack has remaining elements after pop");
last_state.halstead_maps.merge(&state.halstead_maps);
compute_halstead_mi_and_wmc::<T>(last_state, selected);
last_state.space.metrics.merge(&state.space.metrics);
last_state.space.spaces.push(state.space);
}
}
pub fn analyze(source: Source<'_>, options: MetricsOptions) -> Result<FuncSpace, MetricsError> {
Ast::parse(source)?.metrics(options)
}
#[inline]
fn compute_per_node<'a, T: ParserTrait>(
state: &mut State<'a>,
node: &Node<'a>,
code: &'a [u8],
options: MetricsOptions,
func_space: bool,
unit: bool,
nesting_map: &mut HashMap<usize, (usize, usize, usize)>,
) {
let selected = options.metrics;
let last = &mut state.space;
if selected.contains(Metric::Cognitive) {
T::Cognitive::compute(node, code, &mut last.metrics.cognitive, nesting_map);
}
if selected.contains(Metric::Cyclomatic) {
T::Cyclomatic::compute_with_options(
node,
code,
&mut last.metrics.cyclomatic,
options.count_cyclomatic_try,
);
}
if selected.contains(Metric::Halstead) {
T::Halstead::compute(node, code, &mut state.halstead_maps);
}
if selected.contains(Metric::Loc) {
T::Loc::compute(node, &mut last.metrics.loc, func_space, unit);
}
if selected.contains(Metric::Nom) {
T::Nom::compute(node, code, &mut last.metrics.nom);
}
if selected.contains(Metric::Tokens) {
T::Tokens::compute(node, &mut last.metrics.tokens);
}
if selected.contains(Metric::Nargs) {
T::NArgs::compute(node, &mut last.metrics.nargs);
}
if selected.contains(Metric::Nexits) {
T::Exit::compute(node, code, &mut last.metrics.nexits);
}
if selected.contains(Metric::Abc) {
T::Abc::compute(node, code, &mut last.metrics.abc);
}
if selected.contains(Metric::Npm) {
T::Npm::compute(node, code, &mut last.metrics.npm);
}
if selected.contains(Metric::Npa) {
T::Npa::compute(node, code, &mut last.metrics.npa);
}
}
fn push_synthetic_unit_root<T: ParserTrait>(
state_stack: &mut Vec<State>,
node: &Node,
code: &[u8],
selected: MetricSet,
) {
if T::Getter::get_space_kind_with_code(node, code) != SpaceKind::Unit {
let mut synthetic = FuncSpace::new::<T::Getter>(node, code, SpaceKind::Unit, selected);
synthetic
.metrics
.loc
.init_unit_span(node.start_row(), node.end_row());
state_stack.push(State {
space: synthetic,
halstead_maps: HalsteadMaps::new(),
});
}
}
fn apply_comment_suppression<T: ParserTrait>(
state_stack: &mut Vec<State>,
node: &Node,
code: &[u8],
diagnostic_path: &str,
) {
if T::Checker::is_comment(node)
&& let Some(text) = node.utf8_text(code)
{
match parse_suppression_marker(text) {
Ok(Some(s)) => apply_suppression(state_stack, &s),
Ok(None) => {}
Err(e) => {
eprintln!("warning: {}:{}: {e}", diagnostic_path, node.start_row() + 1);
}
}
}
}
pub(crate) fn push_children<'a>(
cursor: &mut Cursor<'a>,
node: &Node<'a>,
new_level: usize,
children: &mut Vec<(Node<'a>, usize)>,
stack: &mut Vec<(Node<'a>, usize)>,
) {
cursor.reset(node);
if cursor.goto_first_child() {
loop {
children.push((cursor.node(), new_level));
if !cursor.goto_next_sibling() {
break;
}
}
for child in children.drain(..).rev() {
stack.push(child);
}
}
}
pub(crate) fn metrics_inner<T: ParserTrait>(
parser: &T,
name: Option<String>,
options: MetricsOptions,
) -> Result<FuncSpace, MetricsError> {
let diagnostic_path = name.as_deref().unwrap_or("<input>");
let selected = options.metrics;
let code = parser.code();
let node = parser.root();
let mut cursor = node.cursor();
let mut stack = Vec::new();
let mut children = Vec::new();
let mut state_stack: Vec<State> = Vec::new();
let mut last_level = 0;
let mut nesting_map = HashMap::<usize, (usize, usize, usize)>::default();
nesting_map.insert(node.id(), (0, 0, 0));
push_synthetic_unit_root::<T>(&mut state_stack, &node, code, selected);
stack.push((node, 0));
while let Some((node, level)) = stack.pop() {
if level < last_level {
finalize::<T>(&mut state_stack, last_level - level, selected);
last_level = level;
}
if options.exclude_tests && T::Checker::should_skip_subtree(&node, code) {
if selected.contains(Metric::Loc)
&& let Some(state) = state_stack.last_mut()
{
state
.space
.metrics
.loc
.exclude_test_span(node.start_row(), node.end_row());
}
continue;
}
let func_space = T::Checker::promotes_to_func_space_with_code(&node, code);
let kind = if func_space || selected.contains(Metric::Loc) {
T::Getter::get_space_kind_with_code(&node, code)
} else {
SpaceKind::Unknown
};
let unit = kind == SpaceKind::Unit;
let new_level = if func_space {
let state = State {
space: FuncSpace::new::<T::Getter>(&node, code, kind, selected),
halstead_maps: HalsteadMaps::new(),
};
state_stack.push(state);
last_level = level + 1;
last_level
} else {
level
};
apply_comment_suppression::<T>(&mut state_stack, &node, code, diagnostic_path);
if let Some(state) = state_stack.last_mut() {
compute_per_node::<T>(
state,
&node,
code,
options,
func_space,
unit,
&mut nesting_map,
);
}
push_children(&mut cursor, &node, new_level, &mut children, &mut stack);
}
finalize::<T>(&mut state_stack, usize::MAX, selected);
let mut state = state_stack.pop().ok_or(MetricsError::EmptyRoot)?;
state.space.name = name;
Ok(state.space)
}
pub(super) fn apply_suppression(state_stack: &mut [State], suppression: &Suppression) {
let target = match suppression.kind {
SuppressionKind::File => state_stack
.iter_mut()
.find(|s| matches!(s.space.kind, SpaceKind::Unit)),
SuppressionKind::Function => state_stack
.iter_mut()
.rev()
.find(|s| matches!(s.space.kind, SpaceKind::Function)),
};
if let Some(state) = target {
state.space.suppressed.merge(&suppression.scope);
}
}