#![allow(clippy::enum_glob_use, clippy::unused_self, clippy::wildcard_imports)]
use serde::Serialize;
use serde::ser::{SerializeStruct, Serializer};
use std::fmt;
use super::cyclomatic;
use super::halstead;
use super::loc;
use crate::checker::Checker;
use crate::macros::implement_metric_trait;
use crate::*;
#[derive(Default, Clone, 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut st = serializer.serialize_struct("maintainability_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(crate) fn merge(&mut self, _other: &Stats) {}
#[inline]
fn inputs_are_empty(&self) -> bool {
self.halstead_volume <= 0.0 || self.sloc <= 0.0
}
#[inline]
#[must_use]
pub fn mi_original(&self) -> f64 {
if self.inputs_are_empty() {
return 0.0;
}
171.0 - 5.2 * (self.halstead_volume).ln() - 0.23 * self.cyclomatic - 16.2 * self.sloc.ln()
}
#[inline]
#[must_use]
pub fn mi_sei(&self) -> f64 {
if self.inputs_are_empty() {
return 0.0;
}
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]
#[must_use]
pub fn mi_visual_studio(&self) -> f64 {
if self.inputs_are_empty() {
return 0.0;
}
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.)
}
}
#[doc(hidden)]
pub trait Mi
where
Self: Checker,
{
fn compute(
loc: &loc::Stats,
cyclomatic: &cyclomatic::Stats,
halstead: &halstead::Stats,
stats: &mut Stats,
) {
stats.halstead_length = halstead.length();
stats.halstead_vocabulary = halstead.vocabulary();
stats.halstead_volume = halstead.volume();
stats.cyclomatic = cyclomatic.cyclomatic_sum();
stats.sloc = loc.sloc();
stats.comments_percentage = if stats.sloc == 0.0 {
0.0
} else {
loc.cloc() / stats.sloc * 100.0
};
}
}
implement_metric_trait!(
[Mi],
PythonCode,
MozjsCode,
JavascriptCode,
TypescriptCode,
TsxCode,
RustCode,
CppCode,
PreprocCode,
CcommentCode,
JavaCode,
KotlinCode,
GoCode,
PerlCode,
BashCode,
LuaCode,
TclCode,
PhpCode,
CsharpCode,
ElixirCode,
RubyCode,
GroovyCode
);
#[cfg(test)]
#[allow(
clippy::float_cmp,
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::similar_names,
clippy::doc_markdown,
clippy::needless_raw_string_hashes,
clippy::too_many_lines
)]
mod tests {
use crate::tools::check_metrics;
use super::*;
#[test]
fn mi_empty_file() {
check_metrics::<PythonParser>("", "empty.py", |metric| {
let mi = &metric.mi;
assert_eq!(mi.mi_original(), 0.0);
assert_eq!(mi.mi_sei(), 0.0);
assert_eq!(mi.mi_visual_studio(), 0.0);
});
}
#[test]
fn check_mi_metrics() {
check_metrics::<PythonParser>(
"def f():
pass",
"foo.py",
|metric| {
insta::assert_json_snapshot!(
metric.mi,
@r###"
{
"mi_original": 151.2033158832232,
"mi_sei": 142.64306171748976,
"mi_visual_studio": 88.42299174457497
}"###
);
},
);
}
#[test]
fn mi_sei_uses_comments_as_percentage() {
let stats = Stats {
halstead_length: 4.0,
halstead_vocabulary: 3.0,
halstead_volume: 4.0 * f64::log2(3.0),
cyclomatic: 1.0,
sloc: 10.0,
comments_percentage: 50.0,
};
let expected = 171.0
- 5.2 * stats.halstead_volume.log2()
- 0.23 * stats.cyclomatic
- 16.2 * stats.sloc.log2()
+ 50.0 * (2.4_f64 * 50.0).sqrt().sin();
let actual = stats.mi_sei();
assert!(
(actual - expected).abs() < 1e-9,
"mi_sei = {actual}, expected {expected}",
);
let buggy = 171.0
- 5.2 * stats.halstead_volume.log2()
- 0.23 * stats.cyclomatic
- 16.2 * stats.sloc.log2()
+ 50.0 * (2.4_f64 * 0.5).sqrt().sin();
assert!(
(actual - buggy).abs() > 50.0,
"mi_sei should differ from the ratio-scaled value by >50; got actual={actual}, buggy={buggy}",
);
}
#[test]
fn rust_mi_smoke() {
check_metrics::<RustParser>("fn f() -> i32 { 1 }\n", "foo.rs", |metric| {
let mi = &metric.mi;
assert!(mi.mi_original() > 0.0);
assert!(mi.mi_sei() > 0.0);
assert!(mi.mi_visual_studio() > 0.0);
});
}
#[test]
fn go_mi_smoke() {
check_metrics::<GoParser>(
"package main\nfunc f() int { return 1 }\n",
"foo.go",
|metric| {
let mi = &metric.mi;
assert!(mi.mi_original() > 0.0);
assert!(mi.mi_sei() > 0.0);
assert!(mi.mi_visual_studio() > 0.0);
},
);
}
#[test]
fn elixir_mi_smoke() {
check_metrics::<ElixirParser>(
"defmodule Foo do\n def f(x), do: x + 1\nend\n",
"foo.ex",
|metric| {
let mi = &metric.mi;
assert!(mi.mi_original() > 0.0);
assert!(mi.mi_sei() > 0.0);
assert!(mi.mi_visual_studio() > 0.0);
},
);
}
#[test]
fn cpp_mi_smoke() {
check_metrics::<CppParser>(
"int f(int x) { if (x > 0) return 1; return 0; }",
"foo.cpp",
|metric| {
let mi = &metric.mi;
assert!(mi.mi_original() > 0.0);
assert!(mi.mi_sei() > 0.0);
assert!(mi.mi_visual_studio() > 0.0);
},
);
}
#[test]
fn javascript_mi_smoke() {
check_metrics::<JavascriptParser>(
"function f(x) { if (x > 0) return 1; return 0; }",
"foo.js",
|metric| {
let mi = &metric.mi;
assert!(mi.mi_original() > 0.0);
assert!(mi.mi_sei() > 0.0);
assert!(mi.mi_visual_studio() > 0.0);
},
);
}
#[test]
fn mozjs_mi_smoke() {
check_metrics::<MozjsParser>(
"function f(x) { if (x > 0) return 1; return 0; }",
"foo.js",
|metric| {
let mi = &metric.mi;
assert!(mi.mi_original() > 0.0);
assert!(mi.mi_sei() > 0.0);
assert!(mi.mi_visual_studio() > 0.0);
},
);
}
}