diff --git a/README.md b/README.md index d71ea8890..f140a7f66 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ control flow of a program. - LLOC: it counts the number of logical lines (instructions) contained in a source file. - HALSTEAD: it is a suite that provides a series of information, such as the effort required to maintain the analyzed code, the size in bits to store the program, the difficulty to understand the code, an estimate of the number of bugs present in the codebase, and an estimate of the time needed to implement the software. - MI: it is a suite that allows to evaluate the maintainability of a software. +- NOM: it counts the number of functions and closures in a file/trait/class. - NEXITS: it counts the number of possible exit points from a method/function. - NARGS: it counts the number of arguments of a function/method. diff --git a/src/lib.rs b/src/lib.rs index 9a8220402..9c2fdcf4f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,6 +40,9 @@ pub use crate::mi::*; pub mod halstead; pub use crate::halstead::*; +pub mod nom; +pub use crate::nom::*; + pub mod fn_args; pub use crate::fn_args::*; diff --git a/src/macros.rs b/src/macros.rs index d0c0156af..381221b2b 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -161,3 +161,17 @@ macro_rules! color { )?; }; } + +#[macro_export] +macro_rules! check_metrics { + ($source: expr, $file: expr, $parser: ident, $metric: ident, + [ $( ( $func: ident, $true_value: expr $(,$type: ty)? )$(,)* )* ]) => { + { + let path = PathBuf::from($file); + let parser = $parser::new($source.to_string().into_bytes(), &path, None); + let func_space = metrics(&parser, &path).unwrap(); + + $( assert_eq!(func_space.metrics.$metric.$func() $(as $type)?, $true_value); )* + } + }; +} diff --git a/src/metrics.rs b/src/metrics.rs index acff96a54..f8cc2ad3a 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -16,6 +16,7 @@ use crate::getter::Getter; use crate::halstead::{self, Halstead}; use crate::loc::{self, Loc}; use crate::mi::{self, Mi}; +use crate::nom::{self, Nom}; use crate::tools::write_file; use crate::traits::*; @@ -24,6 +25,7 @@ pub struct CodeMetrics<'a> { pub cyclomatic: cyclomatic::Stats, pub halstead: halstead::Stats<'a>, pub loc: loc::Stats, + pub nom: nom::Stats, pub mi: mi::Stats, pub nargs: fn_args::Stats, pub nexits: exit::Stats, @@ -38,6 +40,7 @@ impl<'a> Serialize for CodeMetrics<'a> { st.serialize_field("cyclomatic", &self.cyclomatic)?; st.serialize_field("halstead", &self.halstead)?; st.serialize_field("loc", &self.loc)?; + st.serialize_field("nom", &self.nom)?; st.serialize_field("mi", &self.mi)?; st.serialize_field("nargs", &self.nargs)?; st.serialize_field("nexits", &self.nexits)?; @@ -51,6 +54,7 @@ impl<'a> Default for CodeMetrics<'a> { cyclomatic: cyclomatic::Stats::default(), halstead: halstead::Stats::default(), loc: loc::Stats::default(), + nom: nom::Stats::default(), mi: mi::Stats::default(), nargs: fn_args::Stats::default(), nexits: exit::Stats::default(), @@ -63,6 +67,7 @@ impl<'a> fmt::Display for CodeMetrics<'a> { writeln!(f, "{}", self.cyclomatic)?; writeln!(f, "{}", self.halstead)?; writeln!(f, "{}", self.loc)?; + writeln!(f, "{}", self.nom)?; writeln!(f, "{}", self.mi)?; writeln!(f, "{}", self.nargs)?; write!(f, "{}", self.nexits) @@ -74,6 +79,7 @@ impl<'a> CodeMetrics<'a> { self.cyclomatic.merge(&other.cyclomatic); self.halstead.merge(&other.halstead); self.loc.merge(&other.loc); + self.nom.merge(&other.nom); self.mi.merge(&other.mi); self.nargs.merge(&other.nargs); self.nexits.merge(&other.nexits); @@ -190,6 +196,7 @@ impl<'a> FuncSpace<'a> { Self::dump_nexits(&metrics.nexits, &prefix, false, stdout)?; Self::dump_halstead(&metrics.halstead, &prefix, false, stdout)?; Self::dump_loc(&metrics.loc, &prefix, false, stdout)?; + Self::dump_nom(&metrics.nom, &prefix, false, stdout)?; Self::dump_mi(&metrics.mi, &prefix, true, stdout) } @@ -270,6 +277,26 @@ impl<'a> FuncSpace<'a> { Self::dump_value("cloc", stats.cloc(), &prefix, true, stdout) } + fn dump_nom( + stats: &nom::Stats, + prefix: &str, + last: bool, + stdout: &mut StandardStreamLock, + ) -> std::io::Result<()> { + let (pref_child, pref) = if last { (" ", "`- ") } else { ("| ", "|- ") }; + + color!(stdout, Blue); + write!(stdout, "{}{}", prefix, pref)?; + + color!(stdout, Green, true); + writeln!(stdout, "nom")?; + + let prefix = format!("{}{}", prefix, pref_child); + Self::dump_value("functions", stats.functions(), &prefix, false, stdout)?; + Self::dump_value("closures", stats.closures(), &prefix, false, stdout)?; + Self::dump_value("total", stats.total(), &prefix, true, stdout) + } + fn dump_mi( stats: &mi::Stats, prefix: &str, @@ -427,6 +454,7 @@ pub fn metrics<'a, T: TSParserTrait>(parser: &'a T, path: &'a PathBuf) -> Option T::Cyclomatic::compute(&node, &mut last.metrics.cyclomatic); T::Halstead::compute(&node, code, &mut last.metrics.halstead); T::Loc::compute(&node, code, &mut last.metrics.loc, func_space, unit); + T::Nom::compute(&node, &mut last.metrics.nom); T::Mi::compute( &node, &last.metrics.loc, diff --git a/src/nom.rs b/src/nom.rs new file mode 100644 index 000000000..e05838c92 --- /dev/null +++ b/src/nom.rs @@ -0,0 +1,267 @@ +use serde::ser::{SerializeStruct, Serializer}; +use serde::Serialize; +use std::fmt; +use tree_sitter::Node; + +use crate::checker::Checker; + +use crate::*; + +#[derive(Default, Debug)] +pub struct Stats { + functions: usize, + closures: usize, +} + +impl Serialize for Stats { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut st = serializer.serialize_struct("nom", 3)?; + st.serialize_field("functions", &self.functions())?; + st.serialize_field("closures", &self.closures())?; + st.serialize_field("total", &self.total())?; + st.end() + } +} + +impl fmt::Display for Stats { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "functions: {}, \ + closures: {}, \ + total: {}", + self.functions(), + self.closures(), + self.total(), + ) + } +} + +impl Stats { + pub fn merge(&mut self, other: &Stats) { + self.functions += other.functions; + self.closures += other.closures; + } + + #[inline(always)] + pub fn functions(&self) -> f64 { + // Only function definitions are considered, not general declarations + self.functions as f64 + } + + #[inline(always)] + pub fn closures(&self) -> f64 { + self.closures as f64 + } + + #[inline(always)] + pub fn total(&self) -> f64 { + self.functions() + self.closures() + } +} + +pub trait Nom +where + Self: Checker, +{ + fn compute(_node: &Node, _stats: &mut Stats) {} +} + +impl Nom for PythonCode { + fn compute(node: &Node, stats: &mut Stats) { + use Python::*; + + match node.kind_id().into() { + FunctionDefinition => { + stats.functions += 1; + } + Lambda => { + stats.closures += 1; + } + _ => {} + } + } +} + +impl Nom for MozjsCode { + fn compute(node: &Node, stats: &mut Stats) { + use Mozjs::*; + + match node.kind_id().into() { + Function | FunctionDeclaration | MethodDefinition => { + stats.functions += 1; + } + GeneratorFunction | GeneratorFunctionDeclaration | ArrowFunction => { + stats.closures += 1; + } + _ => {} + } + } +} + +impl Nom for JavascriptCode { + fn compute(node: &Node, stats: &mut Stats) { + use Javascript::*; + + match node.kind_id().into() { + Function | FunctionDeclaration | MethodDefinition => { + stats.functions += 1; + } + GeneratorFunction | GeneratorFunctionDeclaration | ArrowFunction => { + stats.closures += 1; + } + _ => {} + } + } +} + +impl Nom for TypescriptCode { + fn compute(node: &Node, stats: &mut Stats) { + use Typescript::*; + + match node.kind_id().into() { + Function | FunctionDeclaration | MethodDefinition => { + stats.functions += 1; + } + GeneratorFunction | GeneratorFunctionDeclaration | ArrowFunction => { + stats.closures += 1; + } + _ => {} + } + } +} + +impl Nom for TsxCode { + fn compute(node: &Node, stats: &mut Stats) { + use Tsx::*; + + match node.kind_id().into() { + Function | FunctionDeclaration | MethodDefinition => { + stats.functions += 1; + } + GeneratorFunction | GeneratorFunctionDeclaration | ArrowFunction => { + stats.closures += 1; + } + _ => {} + } + } +} + +impl Nom for RustCode { + fn compute(node: &Node, stats: &mut Stats) { + use Rust::*; + + match node.kind_id().into() { + FunctionItem => { + stats.functions += 1; + } + ClosureExpression => { + stats.closures += 1; + } + _ => {} + } + } +} + +impl Nom for CppCode { + fn compute(node: &Node, stats: &mut Stats) { + use Cpp::*; + + match node.kind_id().into() { + FunctionDefinition + | FunctionDefinition2 + | FunctionDefinition3 + | FunctionDefinition4 + | FunctionDefinitionRepeat1 => { + stats.functions += 1; + } + LambdaExpression => { + stats.closures += 1; + } + _ => {} + } + } +} + +impl Nom for PreprocCode {} +impl Nom for CcommentCode {} +impl Nom for CSharpCode {} +impl Nom for JavaCode {} +impl Nom for GoCode {} +impl Nom for CssCode {} +impl Nom for HtmlCode {} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use super::*; + + #[test] + fn test_nom_python() { + check_metrics!( + "def a():\n pass\n\n + def b():\n pass\n\n + def c():\n pass\n\n + x = lambda a : a + 42\n", + "foo.py", + PythonParser, + nom, + [ + (functions, 3, usize), + (closures, 1, usize), + (total, 4, usize) + ] + ); + } + + #[test] + fn test_nom_rust() { + check_metrics!( + "mod A { fn foo() {}}\n mod B { fn foo() {}}\n + let closure = |i: i32| -> i32 { i + 42 };\n", + "foo.rs", + RustParser, + nom, + [ + (functions, 2, usize), + (closures, 1, usize), + (total, 3, usize) + ] + ); + } + + #[test] + fn test_nom_cpp() { + check_metrics!( + "struct A {\n void foo(int) {}\n void foo(double) {}\n};\n + int b = [](int x) -> int { return x + 42; };\n", + "foo.cpp", + CppParser, + nom, + [ + (functions, 2, usize), + (closures, 1, usize), + (total, 3, usize) + ] + ); + } + + #[test] + fn test_nom_c() { + check_metrics!( + "int foo();\n\nint foo() {\n return 0;\n}\n", + "foo.c", + CppParser, + nom, + [ + (functions, 1, usize), + (closures, 0, usize), + (total, 1, usize) + ] + ); + } +} diff --git a/src/traits.rs b/src/traits.rs index 15a5ef4b0..9b6ecb3ca 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -11,6 +11,7 @@ use crate::halstead::Halstead; use crate::languages::*; use crate::loc::Loc; use crate::mi::Mi; +use crate::nom::Nom; use crate::preproc::PreprocResults; use crate::ts_parser::Filter; use crate::web::alterator::Alterator; @@ -29,6 +30,7 @@ pub trait TSParserTrait { type Cyclomatic: Cyclomatic; type Halstead: Halstead; type Loc: Loc; + type Nom: Nom; type Mi: Mi; type NArgs: NArgs; type Exit: Exit; diff --git a/src/ts_parser.rs b/src/ts_parser.rs index 116328c3d..5d23b5eaf 100644 --- a/src/ts_parser.rs +++ b/src/ts_parser.rs @@ -13,12 +13,23 @@ use crate::halstead::Halstead; use crate::languages::*; use crate::loc::Loc; use crate::mi::Mi; +use crate::nom::Nom; use crate::preproc::{get_macros, PreprocResults}; use crate::traits::*; use crate::web::alterator::Alterator; pub struct TSParser< - T: TSLanguage + Checker + Getter + Alterator + Cyclomatic + Exit + Halstead + NArgs + Loc + Mi, + T: TSLanguage + + Checker + + Getter + + Alterator + + Cyclomatic + + Exit + + Halstead + + NArgs + + Loc + + Nom + + Mi, > { code: Vec, tree: Tree, @@ -80,6 +91,7 @@ impl< + Exit + Halstead + Loc + + Nom + Mi + NArgs, > TSParserTrait for TSParser @@ -89,6 +101,7 @@ impl< type Cyclomatic = T; type Halstead = T; type Loc = T; + type Nom = T; type Mi = T; type NArgs = T; type Exit = T; diff --git a/src/web/server.rs b/src/web/server.rs index 112a00fc2..ccd3d1e16 100644 --- a/src/web/server.rs +++ b/src/web/server.rs @@ -651,6 +651,7 @@ mod tests { "n1": 2.0, "volume": 4.754_887_502_163_468}, "loc": {"cloc": 1.0, "lloc": 2.0, "sloc": 4.0}, + "nom": {"functions": 1.0, "closures": 0.0, "total": 1.0}, "mi": {"mi_original": 140.204_331_558_152_12, "mi_sei": 161.644_455_240_662_24, "mi_visual_studio": 81.990_837_168_510_01}}, @@ -676,6 +677,7 @@ mod tests { "n1": 2.0, "volume": 4.754_887_502_163_468}, "loc": {"cloc": 0.0, "lloc": 2.0, "sloc": 2.0}, + "nom": {"functions": 1.0, "closures": 0.0, "total": 1.0}, "mi": {"mi_original": 151.433_315_883_223_23, "mi_sei": 142.873_061_717_489_78, "mi_visual_studio": 88.557_494_668_551_6}}, @@ -727,6 +729,7 @@ mod tests { "n1": 2.0, "volume": 4.754_887_502_163_468}, "loc": {"cloc": 0.0, "lloc": 2.0, "sloc": 2.0}, + "nom": {"functions": 1.0, "closures": 0.0, "total": 1.0}, "mi": {"mi_original": 151.433_315_883_223_23, "mi_sei": 142.873_061_717_489_78, "mi_visual_studio": 88.557_494_668_551_6}}, @@ -774,6 +777,7 @@ mod tests { "n1": 2.0, "volume": 4.754_887_502_163_468}, "loc": {"cloc": 0.0, "lloc": 2.0, "sloc": 2.0}, + "nom": {"functions": 1.0, "closures": 0.0, "total": 1.0}, "mi": {"mi_original": 151.433_315_883_223_23, "mi_sei": 142.873_061_717_489_78, "mi_visual_studio": 88.557_494_668_551_6}}, @@ -799,6 +803,7 @@ mod tests { "n1": 2.0, "volume": 4.754_887_502_163_468}, "loc": {"cloc": 0.0, "lloc": 2.0, "sloc": 2.0}, + "nom": {"functions": 1.0, "closures": 0.0, "total": 1.0}, "mi": {"mi_original": 151.433_315_883_223_23, "mi_sei": 142.873_061_717_489_78, "mi_visual_studio": 88.557_494_668_551_6}},