Skip to main content

codelore_rca/
spaces.rs

1use std::collections::HashMap;
2
3use serde::Serialize;
4use std::fmt;
5use std::path::{Path, PathBuf};
6
7use crate::checker::Checker;
8use crate::node::Node;
9
10use crate::cognitive::{self, Cognitive};
11use crate::cyclomatic::{self, Cyclomatic};
12use crate::exit::{self, Exit};
13use crate::getter::Getter;
14use crate::halstead::{self, Halstead, HalsteadMaps};
15use crate::loc::{self, Loc};
16use crate::mi::{self, Mi};
17use crate::nargs::{self, NArgs};
18use crate::nom::{self, Nom};
19
20use crate::dump_metrics::*;
21use crate::traits::*;
22
23/// The list of supported space kinds.
24#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
25#[serde(rename_all = "lowercase")]
26pub enum SpaceKind {
27    /// An unknown space
28    #[default]
29    Unknown,
30    /// A function space
31    Function,
32    /// A class space
33    Class,
34    /// A struct space
35    Struct,
36    /// A `Rust` trait space
37    Trait,
38    /// A `Rust` implementation space
39    Impl,
40    /// A general space
41    Unit,
42    /// A `C/C++` namespace
43    Namespace,
44    /// An interface
45    Interface,
46}
47
48impl fmt::Display for SpaceKind {
49    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
50        let s = match self {
51            SpaceKind::Unknown => "unknown",
52            SpaceKind::Function => "function",
53            SpaceKind::Class => "class",
54            SpaceKind::Struct => "struct",
55            SpaceKind::Trait => "trait",
56            SpaceKind::Impl => "impl",
57            SpaceKind::Unit => "unit",
58            SpaceKind::Namespace => "namespace",
59            SpaceKind::Interface => "interface",
60        };
61        write!(f, "{s}")
62    }
63}
64
65/// All metrics data.
66#[derive(Default, Debug, Clone, Serialize)]
67pub struct CodeMetrics {
68    /// `NArgs` data
69    pub nargs: nargs::Stats,
70    /// `NExits` data
71    pub nexits: exit::Stats,
72    pub cognitive: cognitive::Stats,
73    /// `Cyclomatic` data
74    pub cyclomatic: cyclomatic::Stats,
75    /// `Halstead` data
76    pub halstead: halstead::Stats,
77    /// `Loc` data
78    pub loc: loc::Stats,
79    /// `Nom` data
80    pub nom: nom::Stats,
81    /// `Mi` data
82    pub mi: mi::Stats,
83}
84
85impl fmt::Display for CodeMetrics {
86    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
87        writeln!(f, "{}", self.nargs)?;
88        writeln!(f, "{}", self.nexits)?;
89        writeln!(f, "{}", self.cognitive)?;
90        writeln!(f, "{}", self.cyclomatic)?;
91        writeln!(f, "{}", self.halstead)?;
92        writeln!(f, "{}", self.loc)?;
93        writeln!(f, "{}", self.nom)?;
94        write!(f, "{}", self.mi)
95    }
96}
97
98impl CodeMetrics {
99    pub fn merge(&mut self, other: &CodeMetrics) {
100        self.cognitive.merge(&other.cognitive);
101        self.cyclomatic.merge(&other.cyclomatic);
102        self.halstead.merge(&other.halstead);
103        self.loc.merge(&other.loc);
104        self.nom.merge(&other.nom);
105        self.mi.merge(&other.mi);
106        self.nargs.merge(&other.nargs);
107        self.nexits.merge(&other.nexits);
108    }
109}
110
111/// Function space data.
112#[derive(Debug, Clone, Serialize)]
113pub struct FuncSpace {
114    /// The name of a function space
115    ///
116    /// If `None`, an error is occurred in parsing
117    /// the name of a function space
118    pub name: Option<String>,
119    /// The first line of a function space
120    pub start_line: usize,
121    /// The last line of a function space
122    pub end_line: usize,
123    /// The space kind
124    pub kind: SpaceKind,
125    /// All subspaces contained in a function space
126    pub spaces: Vec<FuncSpace>,
127    /// All metrics of a function space
128    pub metrics: CodeMetrics,
129}
130
131impl FuncSpace {
132    fn new<T: Getter>(node: &Node, code: &[u8], kind: SpaceKind) -> Self {
133        let (start_position, end_position) = match kind {
134            SpaceKind::Unit => {
135                if node.child_count() == 0 {
136                    (0, 0)
137                } else {
138                    (node.start_row() + 1, node.end_row())
139                }
140            }
141            _ => (node.start_row() + 1, node.end_row() + 1),
142        };
143
144        Self {
145            name: T::get_func_space_name(node, code)
146                .map(|name| name.split_whitespace().collect::<Vec<_>>().join(" ")),
147            spaces: Vec::new(),
148            metrics: CodeMetrics::default(),
149            kind,
150            start_line: start_position,
151            end_line: end_position,
152        }
153    }
154}
155
156#[inline(always)]
157fn compute_halstead_and_mi<T: ParserTrait>(state: &mut State) {
158    state
159        .halstead_maps
160        .finalize(&mut state.space.metrics.halstead);
161    T::Mi::compute(
162        &state.space.metrics.loc,
163        &state.space.metrics.cyclomatic,
164        &state.space.metrics.halstead,
165        &mut state.space.metrics.mi,
166    );
167}
168
169#[inline(always)]
170fn compute_averages(state: &mut State) {
171    let nom_functions = state.space.metrics.nom.functions_sum() as usize;
172    let nom_closures = state.space.metrics.nom.closures_sum() as usize;
173    let nom_total = state.space.metrics.nom.total() as usize;
174    // Cognitive average
175    state.space.metrics.cognitive.finalize(nom_total);
176    // Nexit average
177    state.space.metrics.nexits.finalize(nom_total);
178    // Nargs average
179    state
180        .space
181        .metrics
182        .nargs
183        .finalize(nom_functions, nom_closures);
184}
185
186#[inline(always)]
187fn compute_minmax(state: &mut State) {
188    state.space.metrics.cyclomatic.compute_minmax();
189    state.space.metrics.nexits.compute_minmax();
190    state.space.metrics.cognitive.compute_minmax();
191    state.space.metrics.nargs.compute_minmax();
192    state.space.metrics.nom.compute_minmax();
193    state.space.metrics.loc.compute_minmax();
194}
195
196fn finalize<T: ParserTrait>(state_stack: &mut Vec<State>, diff_level: usize) {
197    if state_stack.is_empty() {
198        return;
199    }
200    for _ in 0..diff_level {
201        if state_stack.len() == 1 {
202            let last_state = state_stack.last_mut().unwrap();
203            compute_minmax(last_state);
204            compute_halstead_and_mi::<T>(last_state);
205            compute_averages(last_state);
206            break;
207        } else {
208            let mut state = state_stack.pop().unwrap();
209            compute_minmax(&mut state);
210            compute_halstead_and_mi::<T>(&mut state);
211            compute_averages(&mut state);
212
213            let last_state = state_stack.last_mut().unwrap();
214            last_state.halstead_maps.merge(&state.halstead_maps);
215            compute_halstead_and_mi::<T>(last_state);
216
217            // Merge function spaces
218            last_state.space.metrics.merge(&state.space.metrics);
219            last_state.space.spaces.push(state.space);
220        }
221    }
222}
223
224#[derive(Debug, Clone)]
225struct State<'a> {
226    space: FuncSpace,
227    halstead_maps: HalsteadMaps<'a>,
228}
229
230/// Returns all function spaces data of a code. This function needs a parser to
231/// be created a priori in order to work.
232///
233/// # Examples
234///
235/// ```
236/// use std::path::Path;
237///
238/// use rust_code_analysis::{CppParser, metrics, ParserTrait};
239///
240/// let source_code = "int a = 42;";
241///
242/// // The path to a dummy file used to contain the source code
243/// let path = Path::new("foo.c");
244/// let source_as_vec = source_code.as_bytes().to_vec();
245///
246/// // The parser of the code, in this case a CPP parser
247/// let parser = CppParser::new(source_as_vec, &path, None);
248///
249/// // Gets all function spaces data of the code contained in foo.c
250/// metrics(&parser, &path).unwrap();
251/// ```
252pub fn metrics<'a, T: ParserTrait>(parser: &'a T, path: &'a Path) -> Option<FuncSpace> {
253    let code = parser.get_code();
254    let node = parser.get_root();
255    let mut cursor = node.cursor();
256    let mut stack = Vec::new();
257    let mut children = Vec::new();
258    let mut state_stack: Vec<State> = Vec::new();
259    let mut last_level = 0;
260    // Initialize nesting_map used for storing nesting information for cognitive
261    // Three type of nesting info: conditionals, functions and lambdas
262    let mut nesting_map = HashMap::<usize, (usize, usize, usize)>::default();
263    nesting_map.insert(node.id(), (0, 0, 0));
264    stack.push((node, 0));
265
266    while let Some((node, level)) = stack.pop() {
267        if level < last_level {
268            finalize::<T>(&mut state_stack, last_level - level);
269            last_level = level;
270        }
271
272        let kind = T::Getter::get_space_kind(&node);
273
274        let func_space = T::Checker::is_func(&node) || T::Checker::is_func_space(&node);
275        let unit = kind == SpaceKind::Unit;
276
277        let new_level = if func_space {
278            let state = State {
279                space: FuncSpace::new::<T::Getter>(&node, code, kind),
280                halstead_maps: HalsteadMaps::new(),
281            };
282            state_stack.push(state);
283            last_level = level + 1;
284            last_level
285        } else {
286            level
287        };
288
289        if let Some(state) = state_stack.last_mut() {
290            let last = &mut state.space;
291            T::Cognitive::compute(&node, &mut last.metrics.cognitive, &mut nesting_map);
292            T::Cyclomatic::compute(&node, &mut last.metrics.cyclomatic);
293            T::Halstead::compute(&node, code, &mut state.halstead_maps);
294            T::Loc::compute(&node, &mut last.metrics.loc, func_space, unit);
295            T::Nom::compute(&node, &mut last.metrics.nom);
296            T::NArgs::compute(&node, &mut last.metrics.nargs);
297            T::Exit::compute(&node, &mut last.metrics.nexits);
298        }
299
300        cursor.reset(&node);
301        if cursor.goto_first_child() {
302            loop {
303                children.push((cursor.node(), new_level));
304                if !cursor.goto_next_sibling() {
305                    break;
306                }
307            }
308            for child in children.drain(..).rev() {
309                stack.push(child);
310            }
311        }
312    }
313
314    finalize::<T>(&mut state_stack, usize::MAX);
315
316    state_stack.pop().map(|mut state| {
317        state.space.name = path.to_str().map(|name| name.to_string());
318        state.space
319    })
320}
321
322/// Configuration options for computing
323/// the metrics of a code.
324#[derive(Debug)]
325pub struct MetricsCfg {
326    /// Path to the file containing the code
327    pub path: PathBuf,
328}
329
330pub struct Metrics {
331    _guard: (),
332}
333
334impl Callback for Metrics {
335    type Res = std::io::Result<()>;
336    type Cfg = MetricsCfg;
337
338    fn call<T: ParserTrait>(cfg: Self::Cfg, parser: &T) -> Self::Res {
339        match metrics(parser, &cfg.path) {
340            Some(space) => dump_root(&space),
341            _ => Ok(()),
342        }
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use crate::{CppParser, check_func_space};
349
350    #[test]
351    fn c_scope_resolution_operator() {
352        check_func_space::<CppParser, _>(
353            "void Foo::bar(){
354                return;
355            }",
356            "foo.c",
357            |func_space| {
358                insta::assert_json_snapshot!(
359                    func_space.spaces[0].name,
360                    @r###""Foo::bar""###
361                );
362            },
363        );
364    }
365}