diff --git a/README.md b/README.md index f40a6a69a..f1f55a8d0 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ control flow of a program. - SLOC: it counts the number of lines in a source file. - 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. - 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 07ed54bb0..9a8220402 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,6 +34,9 @@ pub use crate::exit::*; pub mod loc; pub use crate::loc::*; +pub mod mi; +pub use crate::mi::*; + pub mod halstead; pub use crate::halstead::*; diff --git a/src/metrics.rs b/src/metrics.rs index 340d0c97f..b4c67942c 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -15,6 +15,7 @@ use crate::fn_args::{self, NArgs}; use crate::getter::Getter; use crate::halstead::{self, Halstead}; use crate::loc::{self, Loc}; +use crate::mi::{self, Mi}; use crate::tools::write_file; use crate::traits::*; @@ -23,6 +24,7 @@ pub struct CodeMetrics<'a> { pub cyclomatic: cyclomatic::Stats, pub halstead: halstead::Stats<'a>, pub loc: loc::Stats, + pub mi: mi::Stats, pub nargs: fn_args::Stats, pub nexits: exit::Stats, } @@ -36,6 +38,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("mi", &self.mi)?; st.serialize_field("nargs", &self.nargs)?; st.serialize_field("nexits", &self.nexits)?; st.end() @@ -48,6 +51,7 @@ impl<'a> Default for CodeMetrics<'a> { cyclomatic: cyclomatic::Stats::default(), halstead: halstead::Stats::default(), loc: loc::Stats::default(), + mi: mi::Stats::default(), nargs: fn_args::Stats::default(), nexits: exit::Stats::default(), } @@ -59,6 +63,7 @@ impl<'a> fmt::Display for CodeMetrics<'a> { writeln!(f, "{}", self.cyclomatic)?; writeln!(f, "{}", self.halstead)?; writeln!(f, "{}", self.loc)?; + writeln!(f, "{}", self.mi)?; writeln!(f, "{}", self.nargs)?; write!(f, "{}", self.nexits) } @@ -69,6 +74,7 @@ impl<'a> CodeMetrics<'a> { self.cyclomatic.merge(&other.cyclomatic); self.halstead.merge(&other.halstead); self.loc.merge(&other.loc); + self.mi.merge(&other.mi); self.nargs.merge(&other.nargs); self.nexits.merge(&other.nexits); } @@ -183,7 +189,8 @@ impl<'a> FuncSpace<'a> { Self::dump_nargs(&metrics.nargs, &prefix, false, stdout)?; Self::dump_nexits(&metrics.nexits, &prefix, false, stdout)?; Self::dump_halstead(&metrics.halstead, &prefix, false, stdout)?; - Self::dump_loc(&metrics.loc, &prefix, true, stdout) + Self::dump_loc(&metrics.loc, &prefix, false, stdout)?; + Self::dump_mi(&metrics.mi, &prefix, true, stdout) } fn dump_cyclomatic( @@ -263,6 +270,32 @@ impl<'a> FuncSpace<'a> { Self::dump_value("cloc", stats.cloc(), &prefix, true, stdout) } + fn dump_mi( + stats: &mi::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, "mi")?; + + let prefix = format!("{}{}", prefix, pref_child); + Self::dump_value("mi_original", stats.mi_original(), &prefix, false, stdout)?; + Self::dump_value("mi_sei", stats.mi_sei(), &prefix, false, stdout)?; + Self::dump_value( + "mi_visual_studio", + stats.mi_visual_studio(), + &prefix, + true, + stdout, + ) + } + fn dump_nargs( stats: &fn_args::Stats, prefix: &str, @@ -385,6 +418,13 @@ 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::Mi::compute( + &node, + &last.metrics.loc, + &last.metrics.cyclomatic, + &last.metrics.halstead, + &mut last.metrics.mi, + ); T::NArgs::compute(&node, &mut last.metrics.nargs); T::Exit::compute(&node, &mut last.metrics.nexits); } diff --git a/src/mi.rs b/src/mi.rs new file mode 100644 index 000000000..d76884384 --- /dev/null +++ b/src/mi.rs @@ -0,0 +1,113 @@ +use serde::ser::{SerializeStruct, Serializer}; +use serde::Serialize; +use std::fmt; +use tree_sitter::Node; + +use crate::cyclomatic; +use crate::halstead; +use crate::loc; + +use crate::checker::Checker; + +use crate::*; + +#[derive(Default, Debug)] +pub struct Stats { + halstead_length: f64, + halstead_vocabulary: f64, + halstead_volume: f64, + cyclomatic: f64, + sloc: f64, + comments_percentage: f64, +} + +impl Serialize for Stats { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut st = serializer.serialize_struct("maintanability_index", 3)?; + st.serialize_field("mi_original", &self.mi_original())?; + st.serialize_field("mi_sei", &self.mi_sei())?; + st.serialize_field("mi_visual_studio", &self.mi_visual_studio())?; + st.end() + } +} + +impl fmt::Display for Stats { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "mi_original: {}, mi_sei: {}, mi_visual_studio: {}", + self.mi_original(), + self.mi_sei(), + self.mi_visual_studio() + ) + } +} + +impl Stats { + pub fn merge(&mut self, other: &Stats) { + self.halstead_length += other.halstead_length; + self.halstead_vocabulary += other.halstead_vocabulary; + self.halstead_volume = self.halstead_length * self.halstead_vocabulary.log2(); + } + + #[inline(always)] + pub fn mi_original(&self) -> f64 { + // http://www.projectcodemeter.com/cost_estimation/help/GL_maintainability.htm + 171.0 - 5.2 * (self.halstead_volume).ln() - 0.23 * self.cyclomatic - 16.2 * self.sloc.ln() + } + + #[inline(always)] + pub fn mi_sei(&self) -> f64 { + // http://www.projectcodemeter.com/cost_estimation/help/GL_maintainability.htm + 171.0 - 5.2 * self.halstead_volume.log2() - 0.23 * self.cyclomatic - 16.2 * self.sloc.log2() + + 50.0 * (self.comments_percentage * 2.4).sqrt().sin() + } + + #[inline(always)] + pub fn mi_visual_studio(&self) -> f64 { + // http://www.projectcodemeter.com/cost_estimation/help/GL_maintainability.htm + let formula = 171.0 + - 5.2 * self.halstead_volume.ln() + - 0.23 * self.cyclomatic + - 16.2 * self.sloc.ln(); + (formula * 100.0 / 171.0).max(0.) + } +} + +pub trait Mi +where + Self: Checker, +{ + fn compute<'a>( + _node: &Node<'a>, + loc: &loc::Stats, + cyclomatic: &cyclomatic::Stats, + halstead: &halstead::Stats<'a>, + stats: &mut Stats, + ) { + stats.halstead_length = halstead.length(); + stats.halstead_vocabulary = halstead.vocabulary(); + stats.halstead_volume = halstead.volume(); + stats.cyclomatic = cyclomatic.cyclomatic(); + stats.sloc = loc.sloc(); + stats.comments_percentage = loc.cloc() / stats.sloc; + } +} + +impl Mi for RustCode {} +impl Mi for CppCode {} +impl Mi for PythonCode {} +impl Mi for MozjsCode {} +impl Mi for JavascriptCode {} +impl Mi for TypescriptCode {} +impl Mi for TsxCode {} +impl Mi for PreprocCode {} +impl Mi for CcommentCode {} +impl Mi for CSharpCode {} +impl Mi for JavaCode {} +impl Mi for GoCode {} +impl Mi for CssCode {} +impl Mi for HtmlCode {} diff --git a/src/traits.rs b/src/traits.rs index d46f89359..15a5ef4b0 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -10,6 +10,7 @@ use crate::getter::Getter; use crate::halstead::Halstead; use crate::languages::*; use crate::loc::Loc; +use crate::mi::Mi; use crate::preproc::PreprocResults; use crate::ts_parser::Filter; use crate::web::alterator::Alterator; @@ -28,6 +29,7 @@ pub trait TSParserTrait { type Cyclomatic: Cyclomatic; type Halstead: Halstead; type Loc: Loc; + type Mi: Mi; type NArgs: NArgs; type Exit: Exit; diff --git a/src/ts_parser.rs b/src/ts_parser.rs index 6deb9a1a6..116328c3d 100644 --- a/src/ts_parser.rs +++ b/src/ts_parser.rs @@ -12,12 +12,13 @@ use crate::getter::Getter; use crate::halstead::Halstead; use crate::languages::*; use crate::loc::Loc; +use crate::mi::Mi; 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, + T: TSLanguage + Checker + Getter + Alterator + Cyclomatic + Exit + Halstead + NArgs + Loc + Mi, > { code: Vec, tree: Tree, @@ -79,6 +80,7 @@ impl< + Exit + Halstead + Loc + + Mi + NArgs, > TSParserTrait for TSParser { @@ -87,6 +89,7 @@ impl< type Cyclomatic = T; type Halstead = T; type Loc = T; + type Mi = T; type NArgs = T; type Exit = T; diff --git a/src/web/server.rs b/src/web/server.rs index 99613c50c..112a00fc2 100644 --- a/src/web/server.rs +++ b/src/web/server.rs @@ -650,7 +650,10 @@ mod tests { "n2": 1.0, "n1": 2.0, "volume": 4.754_887_502_163_468}, - "loc": {"cloc": 1.0, "lloc": 2.0, "sloc": 4.0}}, + "loc": {"cloc": 1.0, "lloc": 2.0, "sloc": 4.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}}, "name": "test.py", "spaces": [{"kind": "function", "start_line": 3, @@ -672,7 +675,10 @@ mod tests { "n2": 1.0, "n1": 2.0, "volume": 4.754_887_502_163_468}, - "loc": {"cloc": 0.0, "lloc": 2.0, "sloc": 2.0}}, + "loc": {"cloc": 0.0, "lloc": 2.0, "sloc": 2.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}}, "name": "foo", "spaces": []}]} }); @@ -720,7 +726,10 @@ mod tests { "n2": 1.0, "n1": 2.0, "volume": 4.754_887_502_163_468}, - "loc": {"cloc": 0.0, "lloc": 2.0, "sloc": 2.0}}, + "loc": {"cloc": 0.0, "lloc": 2.0, "sloc": 2.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}}, "name": "test.py", "spaces": []} }); @@ -764,7 +773,10 @@ mod tests { "n2": 1.0, "n1": 2.0, "volume": 4.754_887_502_163_468}, - "loc": {"cloc": 0.0, "lloc": 2.0, "sloc": 2.0}}, + "loc": {"cloc": 0.0, "lloc": 2.0, "sloc": 2.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}}, "name": "test.py", "spaces": [{"kind": "function", "start_line": 1, @@ -786,7 +798,10 @@ mod tests { "n2": 1.0, "n1": 2.0, "volume": 4.754_887_502_163_468}, - "loc": {"cloc": 0.0, "lloc": 2.0, "sloc": 2.0}}, + "loc": {"cloc": 0.0, "lloc": 2.0, "sloc": 2.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}}, "name": "foo", "spaces": []}]} });