1#![allow(clippy::enum_glob_use, clippy::unused_self, clippy::wildcard_imports)]
8#![allow(clippy::cast_precision_loss)]
12
13use std::fmt;
14
15use super::cyclomatic;
16use super::halstead;
17use super::loc;
18
19use crate::checker::Checker;
20use crate::macros::implement_metric_trait;
21
22use crate::*;
23
24#[derive(Default, Clone, Debug, PartialEq)]
26#[non_exhaustive]
27pub struct Stats {
28 halstead_length: f64,
29 halstead_vocabulary: f64,
30 halstead_volume: f64,
31 cyclomatic: f64,
32 code_lines: f64,
36 comment_ratio: f64,
42}
43
44impl fmt::Display for Stats {
45 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
46 write!(
47 f,
48 "original: {}, sei: {}, visual_studio: {}",
49 self.original(),
50 self.sei(),
51 self.visual_studio()
52 )
53 }
54}
55
56impl Stats {
57 pub(crate) fn merge(&mut self, _other: &Stats) {}
64
65 #[inline]
74 #[must_use]
75 pub fn is_empty(&self) -> bool {
76 self.halstead_volume <= 0.0 || self.code_lines <= 0.0
77 }
78
79 #[inline]
85 #[must_use]
86 pub fn original(&self) -> f64 {
87 if self.is_empty() {
88 return 0.0;
89 }
90 171.0
92 - 5.2 * (self.halstead_volume).ln()
93 - 0.23 * self.cyclomatic
94 - 16.2 * self.code_lines.ln()
95 }
96
97 #[inline]
106 #[must_use]
107 pub fn sei(&self) -> f64 {
108 if self.is_empty() {
109 return 0.0;
110 }
111 self.original() + 50.0 * (2.4 * self.comment_ratio).sqrt().sin()
112 }
113
114 #[inline]
119 #[must_use]
120 pub fn visual_studio(&self) -> f64 {
121 (self.original() * 100.0 / 171.0).max(0.)
124 }
125}
126
127#[doc(hidden)]
128pub(crate) trait Mi
130where
131 Self: Checker,
132{
133 fn compute(
137 loc: &loc::Stats,
138 cyclomatic: &cyclomatic::Stats,
139 halstead: &halstead::Stats,
140 stats: &mut Stats,
141 charges_docs: bool,
142 ) {
143 stats.halstead_length = halstead.length() as f64;
144 stats.halstead_vocabulary = halstead.vocabulary() as f64;
145 stats.halstead_volume = halstead.volume();
146 stats.cyclomatic = cyclomatic.cyclomatic_sum() as f64;
147 stats.code_lines = loc.ploc() as f64;
153 let (comment_lines, physical_lines) = if charges_docs {
163 (loc.cloc(), loc.sloc())
164 } else {
165 (
166 loc.cloc().saturating_sub(loc.dloc()),
167 loc.sloc().saturating_sub(loc.dloc_without_code()),
168 )
169 };
170 stats.comment_ratio = if physical_lines == 0 {
171 0.0
172 } else {
173 (comment_lines as f64 / physical_lines as f64).clamp(0.0, 1.0)
176 };
177 }
178}
179
180implement_metric_trait!(
187 [Mi],
188 PythonCode,
189 MozjsCode,
190 JavascriptCode,
191 TypescriptCode,
192 TsxCode,
193 RustCode,
194 CppCode,
195 MozcppCode,
196 CCode,
197 ObjcCode,
198 PreprocCode,
199 CcommentCode,
200 JavaCode,
201 KotlinCode,
202 GoCode,
203 PerlCode,
204 BashCode,
205 LuaCode,
206 TclCode,
207 PhpCode,
208 CsharpCode,
209 ElixirCode,
210 RubyCode,
211 GroovyCode,
212 IrulesCode
213);
214
215#[cfg(test)]
216#[allow(
217 clippy::float_cmp,
218 clippy::cast_precision_loss,
219 clippy::cast_possible_truncation,
220 clippy::cast_sign_loss,
221 clippy::similar_names,
222 clippy::doc_markdown,
223 clippy::needless_raw_string_hashes,
224 clippy::too_many_lines
225)]
226mod tests {
227 use crate::test_support::check_metrics_only_shim;
228 #[cfg(any(feature = "python", all(feature = "rust", feature = "java")))]
229 use crate::test_support::metrics_verbatim;
230
231 use super::*;
232
233 check_metrics_only_shim!(check_metrics, Mi);
238
239 #[cfg(feature = "python")]
243 fn python_mi(source: &[u8]) -> CodeMetrics {
244 metrics_verbatim(
245 crate::LANG::Python,
246 source,
247 MetricsOptions::default().with_only(&[Metric::Mi]),
248 )
249 }
250
251 #[cfg(feature = "python")]
254 fn python_modes(source: &[u8]) -> (CodeMetrics, CodeMetrics) {
255 let read = |charges_docs: bool| {
256 metrics_verbatim(
257 crate::LANG::Python,
258 source,
259 MetricsOptions::default().with_mi_charges_docs(charges_docs),
260 )
261 };
262 (read(false), read(true))
263 }
264
265 #[cfg(feature = "python")]
268 const DOCUMENTED: &[u8] = b"def clamp(value, low, high):\n \"\"\"Return ``value`` held within ``[low, high]``.\n\n The bounds are inclusive and are not checked against each\n other, so ``low > high`` returns ``high`` for every input.\n \"\"\"\n if value < low:\n return low\n if value > high:\n return high\n return value\n";
269
270 #[cfg(any(
275 feature = "cpp",
276 feature = "elixir",
277 feature = "go",
278 feature = "javascript",
279 feature = "mozjs",
280 feature = "rust"
281 ))]
282 #[track_caller]
283 fn assert_comment_free_mi(
284 m: &CodeMetrics,
285 inputs: (u64, u64, u64, u64),
286 original: f64,
287 visual_studio: f64,
288 ) {
289 assert_eq!(
290 (
291 m.halstead.length(),
292 m.halstead.vocabulary(),
293 m.cyclomatic.cyclomatic_sum(),
294 m.loc.ploc()
295 ),
296 inputs,
297 "(N, n, G, ploc)"
298 );
299 assert_eq!(m.loc.cloc(), 0, "the fixture is comment-free");
300 assert!(
301 (m.mi.original() - original).abs() < 1e-9,
302 "original {} vs {original}",
303 m.mi.original()
304 );
305 assert!(
306 (m.mi.visual_studio() - visual_studio).abs() < 1e-9,
307 "visual_studio {} vs {visual_studio}",
308 m.mi.visual_studio()
309 );
310 assert_eq!(m.mi.sei().to_bits(), m.mi.original().to_bits());
311 }
312
313 #[cfg(feature = "python")]
314 #[test]
315 fn mi_empty_file() {
316 check_metrics::<PythonParser>("", "empty.py", |metric| {
317 let mi = &metric.mi;
318 assert_eq!(mi.original(), 0.0);
319 assert_eq!(mi.sei(), 0.0);
320 assert_eq!(mi.visual_studio(), 0.0);
321 });
322 }
323
324 #[test]
325 fn empty_inputs_are_empty_and_every_variant_is_the_sentinel() {
326 let mut mi = Stats::default();
327 assert!(mi.is_empty());
328 assert_eq!([mi.original(), mi.sei(), mi.visual_studio()], [0.0; 3]);
329 mi.halstead_volume = 10.0;
331 assert!(mi.is_empty(), "zero size term");
332 mi.halstead_volume = 0.0;
333 mi.code_lines = 3.0;
334 assert!(mi.is_empty(), "zero Halstead volume");
335 mi.halstead_volume = 10.0;
336 assert!(!mi.is_empty());
337 assert!(mi.original() > 0.0);
338 }
339
340 #[cfg(feature = "python")]
341 #[test]
342 fn a_trivial_function_is_not_empty() {
343 check_metrics::<PythonParser>("def f():\n pass\n", "foo.py", |metric| {
344 assert!(!metric.mi.is_empty());
345 assert!(metric.mi.original() > 0.0);
346 });
347 }
348
349 #[cfg(feature = "cpp")]
353 #[test]
354 fn an_include_only_file_is_empty() {
355 check_metrics::<CppParser>(
356 "#include <vector>\n#include \"fst.h\"\n",
357 "fstlib.h",
358 |metric| {
359 assert_eq!(metric.loc.sloc(), 2);
360 assert!(metric.mi.is_empty());
361 assert_eq!(metric.mi.original(), 0.0);
362 },
363 );
364 }
365
366 #[cfg(feature = "python")]
367 #[test]
368 fn check_mi_metrics() {
369 check_metrics::<PythonParser>(
372 "def f():
373 pass",
374 "foo.py",
375 |metric| {
376 insta::assert_json_snapshot!(
378 metric.mi,
379 @r#"
380 {
381 "original": 146.56146936754593,
382 "sei": 146.56146936754593,
383 "visual_studio": 85.70846161844791
384 }
385 "#
386 );
387 },
388 );
389 }
390
391 #[cfg(feature = "python")]
397 #[test]
398 fn python_docstring_does_not_move_mi() {
399 let undocumented = b"def clamp(value, low, high):\n if value < low:\n return low\n if value > high:\n return high\n return value\n";
400 let documented = b"def clamp(value, low, high):\n \"\"\"Return ``value`` held within ``[low, high]``.\n\n ``low`` and ``high`` are inclusive bounds; a value outside them is\n replaced by the nearer bound, and a value inside is returned as it\n is. The bounds are not checked against each other, so ``low > high``\n returns ``high`` for every input.\n \"\"\"\n if value < low:\n return low\n if value > high:\n return high\n return value\n";
401 let bare = python_mi(undocumented);
402 let doc = python_mi(documented);
403 assert_eq!((doc.loc.dloc(), doc.loc.cloc(), doc.loc.sloc()), (7, 7, 13));
405 assert_eq!((bare.loc.dloc(), bare.loc.sloc()), (0, 6));
406 assert_eq!(
407 (doc.mi.original(), doc.mi.sei(), doc.mi.visual_studio()),
408 (bare.mi.original(), bare.mi.sei(), bare.mi.visual_studio()),
409 "a docstring must not move any MI variant"
410 );
411 assert!((bare.mi.original() - 117.676_252_636_666_53).abs() < 1e-9);
414 }
415
416 #[cfg(feature = "python")]
431 #[test]
432 fn charging_docs_moves_only_the_sei_comment_share() {
433 fn child(root: &FuncSpace) -> &CodeMetrics {
434 &root
435 .spaces
436 .iter()
437 .find(|s| s.name.as_deref() == Some("clamp"))
438 .expect("the function space")
439 .metrics
440 }
441 let space = |charges_docs: bool| {
442 crate::test_support::space_verbatim(
443 crate::LANG::Python,
444 DOCUMENTED,
445 MetricsOptions::default().with_mi_charges_docs(charges_docs),
446 )
447 };
448 let (default, charged) = (space(false), space(true));
449 let pairs = [
450 (
451 &default.metrics,
452 &charged.metrics,
453 (4, 117.676_252_636_666_53, 160.909_070_973_313_36),
454 ),
455 (
456 child(&default),
457 child(&charged),
458 (3, 117.906_252_636_666_52, 161.139_070_973_313_32),
459 ),
460 ];
461 let close = |a: f64, b: f64| (a - b).abs() < 1e-9;
462 for (d, c, (cyclomatic, original, charged_sei)) in pairs {
463 assert_eq!(
464 (d.loc.ploc(), d.loc.cloc(), d.loc.sloc(), d.loc.dloc()),
465 (6, 5, 11, 5),
466 "the fixture is documented"
467 );
468 assert_eq!((d.halstead.length(), d.halstead.vocabulary()), (25, 12));
469 assert_eq!(d.cyclomatic.cyclomatic_sum(), cyclomatic);
470 assert!(close(d.mi.original(), original), "{}", d.mi.original());
471 assert!(close(c.mi.sei(), charged_sei), "{}", c.mi.sei());
472 assert_eq!(d.mi.original().to_bits(), c.mi.original().to_bits());
473 assert_eq!(
474 d.mi.visual_studio().to_bits(),
475 c.mi.visual_studio().to_bits()
476 );
477 assert_eq!(d.mi.sei(), d.mi.original(), "no working comment");
478 assert!(
479 c.mi.sei() > d.mi.sei(),
480 "charged {} must exceed default {}",
481 c.mi.sei(),
482 d.mi.sei()
483 );
484 }
485 }
486
487 #[cfg(feature = "python")]
490 #[test]
491 fn a_shared_doc_code_row_sizes_the_same_in_both_modes() {
492 let source = b"def f(): \"\"\"doc\"\"\"; return 1\n";
493 let (d, c) = python_modes(source);
494 assert_eq!((d.loc.dloc(), d.loc.ploc()), (1, 1));
495 assert_eq!(d.mi.code_lines, 1.0);
496 assert_eq!(c.mi.code_lines, 1.0);
497 assert_eq!(d.mi.original(), c.mi.original());
498 assert!(d.mi.original() > 0.0, "not the empty-input sentinel");
499 }
500
501 #[cfg(feature = "python")]
505 #[test]
506 fn blank_rows_move_only_a_live_comment_share() {
507 let commented =
508 python_mi(b"def f(n):\n # c\n if n:\n return 1\n return 0\n");
509 let commented_blanks =
510 python_mi(b"def f(n):\n # c\n\n\n if n:\n return 1\n return 0\n");
511 assert_eq!(commented_blanks.loc.blank(), commented.loc.blank() + 2);
512 assert_eq!(commented.mi.original(), commented_blanks.mi.original());
513 assert_eq!(
514 commented.mi.visual_studio(),
515 commented_blanks.mi.visual_studio()
516 );
517 assert!(
518 commented_blanks.mi.sei() < commented.mi.sei(),
519 "{} must be below {}",
520 commented_blanks.mi.sei(),
521 commented.mi.sei()
522 );
523
524 let bare = python_mi(b"def f(n):\n if n:\n return 1\n return 0\n");
525 let bare_blanks = python_mi(b"def f(n):\n\n\n if n:\n return 1\n return 0\n");
526 assert_eq!(bare_blanks.loc.blank(), bare.loc.blank() + 2);
527 let mi = |m: &CodeMetrics| (m.mi.original(), m.mi.sei(), m.mi.visual_studio());
528 assert_eq!(mi(&bare), mi(&bare_blanks));
529 assert!(bare.mi.original() > 0.0);
530 }
531
532 #[cfg(all(feature = "python", feature = "rust", feature = "c"))]
546 #[test]
547 fn mi_values_derive_from_ploc_and_the_comment_ratio() {
548 type Case = (crate::LANG, &'static [u8], (u64, u64), [(f64, f64, f64); 2]);
550 let cases: [Case; 3] = [
551 (
552 crate::LANG::Python,
553 b"def f(x):\n # note\n\n\n return x + 1\n",
554 (9, 8),
555 [
556 (
557 142.172_663_971_706_38,
558 174.108_154_504_556_7,
559 83.141_908_755_383_85,
560 ),
561 (
562 142.402_663_971_706_4,
563 174.338_154_504_556_7,
564 83.276_411_679_360_48,
565 ),
566 ],
567 ),
568 (
569 crate::LANG::Rust,
570 b"fn f(x: i32) -> i32 {\n // note\n\n\n x + 1 }\n",
571 (12, 10),
572 [
573 (
574 140.146_665_193_717_27,
575 172.082_155_726_567_58,
576 81.957_114_148_372_67,
577 ),
578 (
579 140.376_665_193_717_3,
580 172.312_155_726_567_6,
581 82.091_617_072_349_3,
582 ),
583 ],
584 ),
585 (
586 crate::LANG::C,
587 b"int f(int x) {\n // note\n\n\n return x + 1; }\n",
588 (11, 9),
589 [
590 (
591 140.842_679_026_834_13,
592 172.778_169_559_684_44,
593 82.364_139_781_774_36,
594 ),
595 (
596 141.072_679_026_834_15,
597 173.008_169_559_684_46,
598 82.498_642_705_750_97,
599 ),
600 ],
601 ),
602 ];
603 let close = |a: f64, b: f64| (a - b).abs() < 1e-9;
604 for (lang, source, (length, vocabulary), expected) in cases {
605 let root = crate::test_support::space_verbatim(lang, source, MetricsOptions::default());
606 let f = root
607 .spaces
608 .iter()
609 .find(|s| s.name.as_deref() == Some("f"))
610 .unwrap_or_else(|| panic!("{lang:?}: the function space"));
611 for ((m, cyclomatic), (original, sei, vs)) in [(&root.metrics, 2), (&f.metrics, 1)]
612 .into_iter()
613 .zip(expected)
614 {
615 assert_eq!(
616 (m.loc.ploc(), m.loc.cloc(), m.loc.blank(), m.loc.sloc()),
617 (2, 1, 2, 5),
618 "{lang:?}: the fixture's rows"
619 );
620 assert_eq!(
621 (
622 m.halstead.length(),
623 m.halstead.vocabulary(),
624 m.cyclomatic.cyclomatic_sum()
625 ),
626 (length, vocabulary, cyclomatic),
627 "{lang:?}: the Halstead and cyclomatic inputs"
628 );
629 assert!(
630 close(m.mi.original(), original)
631 && close(m.mi.sei(), sei)
632 && close(m.mi.visual_studio(), vs),
633 "{lang:?}: {} / {} / {} vs {original} / {sei} / {vs}",
634 m.mi.original(),
635 m.mi.sei(),
636 m.mi.visual_studio()
637 );
638 }
639 }
640 }
641 #[cfg(feature = "python")]
652 #[test]
653 fn each_mode_pairs_its_comment_rows_with_its_own_rows() {
654 let source = b"def f(n):\n \"\"\"Doc one.\n Doc two.\n \"\"\"\n # a real comment\n if n:\n return 1\n return 0\n";
655 let (d, c) = python_modes(source);
656 assert_eq!(
657 (d.loc.ploc(), d.loc.cloc(), d.loc.sloc(), d.loc.dloc()),
658 (4, 4, 8, 3)
659 );
660 assert_eq!(d.loc.dloc_without_code(), 3);
661 assert_eq!((d.halstead.length(), d.halstead.vocabulary()), (12, 9));
662 assert_eq!(d.cyclomatic.cyclomatic_sum(), 3);
663 let close = |a: f64, b: f64| (a - b).abs() < 1e-9;
664 for m in [&d, &c] {
665 assert!(close(m.mi.original(), 128.931_235_541_416_98));
666 }
667 assert!(close(d.mi.sei(), 160.866_726_074_267_3), "{}", d.mi.sei());
668 assert!(close(c.mi.sei(), 173.387_837_747_781_2), "{}", c.mi.sei());
669 }
670
671 #[cfg(all(feature = "python", feature = "rust"))]
675 #[test]
676 fn charging_docs_moves_only_documented_spaces_mi() {
677 let cases: &[(crate::LANG, &[u8], bool)] = &[
678 (
680 crate::LANG::Python,
681 b"def f(n):\n # a real comment\n if n:\n return 1\n return 0\n",
682 false,
683 ),
684 (
685 crate::LANG::Python,
686 b"def f(n):\n \"\"\"Doc.\"\"\"\n if n:\n return 1\n return 0\n",
687 true,
688 ),
689 (
690 crate::LANG::Rust,
691 b"/// Doc.\nfn f(x: i32) -> i32 {\n if x > 0 { x } else { 0 }\n}\n",
692 true,
693 ),
694 (crate::LANG::Rust, b"fn f() {}\n", false),
695 (crate::LANG::Python, b"", false),
696 ];
697 for (lang, source, documented) in cases {
698 let excluded = metrics_verbatim(*lang, source, MetricsOptions::default());
699 let explicit = metrics_verbatim(
700 *lang,
701 source,
702 MetricsOptions::default().with_mi_charges_docs(false),
703 );
704 let charged = metrics_verbatim(
705 *lang,
706 source,
707 MetricsOptions::default().with_mi_charges_docs(true),
708 );
709 assert_eq!(excluded, explicit, "{lang:?}: the default is false");
710 assert_eq!((excluded.loc.dloc() > 0), *documented, "{lang:?}: fixture");
711 let mi = |m: &CodeMetrics| (m.mi.original(), m.mi.sei(), m.mi.visual_studio());
712 assert_eq!(
713 mi(&excluded) != mi(&charged),
714 *documented,
715 "{lang:?}: only documentation rows move mi"
716 );
717 let mut without_mi = charged.clone();
718 without_mi.mi = excluded.mi.clone();
719 assert_eq!(without_mi, excluded, "{lang:?}: nothing but mi moves");
720 }
721 }
722
723 #[cfg(feature = "python")]
724 #[test]
725 fn python_additional_docstrings_do_not_move_mi() {
726 for (bare, documented, rows) in [
727 (
728 "def f():\n return 1\n",
729 "def f():\n \"\"\"F.\"\"\"\n \"\"\"More.\"\"\"\n \"\"\"Still more.\"\"\"\n return 1\n",
730 3,
731 ),
732 ("x = 1\n", "x = 1\n\"\"\"X.\"\"\"\n\"\"\"More.\"\"\"\n", 2),
733 ] {
734 let bare = python_mi(bare.as_bytes());
735 let documented = python_mi(documented.as_bytes());
736 assert_eq!(documented.loc.dloc(), rows);
737 assert!(bare.mi.original() > 0.0);
738 assert_eq!(
739 (bare.mi.original(), bare.mi.sei(), bare.mi.visual_studio()),
740 (
741 documented.mi.original(),
742 documented.mi.sei(),
743 documented.mi.visual_studio(),
744 )
745 );
746 }
747 }
748
749 #[cfg(feature = "python")]
756 #[test]
757 fn python_docstring_is_invisible_to_the_sei_comment_term() {
758 let commented =
759 b"def f(n):\n # a real comment\n if n:\n return 1\n return 0\n";
760 let both = b"def f(n):\n \"\"\"Doc one.\n Doc two.\n \"\"\"\n # a real comment\n if n:\n return 1\n return 0\n";
761 let a = python_mi(commented);
762 let b = python_mi(both);
763 assert_eq!((a.loc.cloc(), a.loc.dloc()), (1, 0));
764 assert_eq!((b.loc.cloc(), b.loc.dloc()), (4, 3));
765 assert_eq!(
766 a.mi.sei(),
767 b.mi.sei(),
768 "sei must see the one comment row in both"
769 );
770 assert_eq!(a.mi.original(), b.mi.original());
771 let bare = python_mi(b"def f(n):\n if n:\n return 1\n return 0\n");
774 assert_ne!(
775 a.mi.sei(),
776 bare.mi.sei(),
777 "the comment row must move sei on its own"
778 );
779 }
780
781 #[cfg(feature = "python")]
789 #[test]
790 fn python_docstring_sharing_a_code_row_keeps_the_row() {
791 let fixtures: [(&[u8], u64, f64); 3] = [
793 (b"def f(x): \"\"\"Return x.\"\"\"\n", 1, 1.0),
794 (b"def f(x): \"\"\"Return x.\"\"\"; return x\n", 1, 1.0),
795 (
796 b"def f(x):\n \"\"\"Doc.\n More.\"\"\"; y = 1\n return x\n",
797 2,
798 3.0,
799 ),
800 ];
801 for (source, dloc, code_lines) in fixtures {
802 let m = python_mi(source);
803 assert_eq!((m.loc.dloc(), m.loc.cloc()), (dloc, dloc));
804 let volume = m.halstead.volume();
805 let cyclomatic = m.cyclomatic.cyclomatic_sum() as f64;
806 let original = 171.0 - 5.2 * volume.ln() - 0.23 * cyclomatic - 16.2 * code_lines.ln();
807 let sei = original;
809 assert!(
810 (m.mi.original() - original).abs() < 1e-9 && (m.mi.sei() - sei).abs() < 1e-9,
811 "{:?}: original {} / sei {} must size on {code_lines} code rows ({original} / {sei})",
812 std::str::from_utf8(source),
813 m.mi.original(),
814 m.mi.sei()
815 );
816 assert!(m.mi.original() > 0.0, "not the empty-input sentinel");
817 }
818 }
819
820 #[cfg(feature = "python")]
824 #[test]
825 fn python_code_commented_out_in_a_string_is_charged_like_a_comment() {
826 let in_string = b"def f(n):\n if n:\n return 1\n \"\"\"\n old = n * 2\n return old\n \"\"\"\n return 0\n";
827 let with_hash = b"def f(n):\n if n:\n return 1\n #\n # old = n * 2\n # return old\n #\n return 0\n";
828 let a = python_mi(in_string);
829 let b = python_mi(with_hash);
830 assert_eq!(
831 (a.loc.cloc(), a.loc.dloc()),
832 (4, 0),
833 "the string is comment rows, not docs"
834 );
835 assert_eq!((b.loc.cloc(), b.loc.dloc()), (4, 0));
836 assert_eq!(
837 (a.mi.original(), a.mi.sei(), a.mi.visual_studio()),
838 (b.mi.original(), b.mi.sei(), b.mi.visual_studio()),
839 "a string-wrapped and a #-commented block must cost the same"
840 );
841 }
842
843 #[cfg(all(feature = "rust", feature = "java"))]
847 #[test]
848 fn doc_comments_do_not_move_mi_in_other_languages() {
849 let cases: &[(crate::LANG, &[u8], &[u8], u64)] = &[
850 (
851 crate::LANG::Rust,
852 b"fn f(x: i32) -> i32 {\n if x > 0 { x } else { 0 }\n}\n",
853 b"/// Clamp `x` at zero.\n///\n/// Negative inputs collapse to zero.\nfn f(x: i32) -> i32 {\n if x > 0 { x } else { 0 }\n}\n",
854 3,
855 ),
856 (
857 crate::LANG::Java,
858 b"class C {\n int f(int x) {\n if (x > 0) { return x; }\n return 0;\n }\n}\n",
859 b"class C {\n /**\n * Clamp x at zero.\n */\n int f(int x) {\n if (x > 0) { return x; }\n return 0;\n }\n}\n",
860 3,
861 ),
862 ];
863 let mi_only = || MetricsOptions::default().with_only(&[Metric::Mi]);
864 for (lang, bare, documented, rows) in cases {
865 let a = metrics_verbatim(*lang, bare, mi_only());
866 let b = metrics_verbatim(*lang, documented, mi_only());
867 assert_eq!(
868 (b.loc.dloc(), b.loc.cloc()),
869 (*rows, *rows),
870 "{lang:?}: the doc rows"
871 );
872 assert_eq!(a.loc.dloc(), 0);
873 assert_eq!(
874 (a.mi.original(), a.mi.sei(), a.mi.visual_studio()),
875 (b.mi.original(), b.mi.sei(), b.mi.visual_studio()),
876 "{lang:?}: a doc comment must not move any MI variant"
877 );
878 assert!(
879 a.mi.original() > 0.0,
880 "{lang:?}: the parity is not two zeros agreeing"
881 );
882 }
883 }
884
885 fn ratio_stats(comment_ratio: f64) -> Stats {
886 Stats {
887 halstead_length: 4.0,
888 halstead_vocabulary: 3.0,
889 halstead_volume: 4.0 * f64::log2(3.0),
890 cyclomatic: 1.0,
891 code_lines: 10.0,
892 comment_ratio,
893 }
894 }
895
896 #[test]
901 fn mi_sei_comment_term_is_monotonic_and_bounded() {
902 const CEILING: f64 = 49.988_333_226_140_14;
905 let mut previous = None;
906 for (cm, expected) in [
907 (0.0, 0.0),
908 (0.1, 23.526_792_118_944_968),
909 (0.3, 37.515_409_360_932_6),
910 (0.5, 44.456_602_206_364_21),
911 (0.7, 48.126_033_588_861_63),
912 (1.0, CEILING),
913 ] {
914 let stats = ratio_stats(cm);
915 let term = stats.sei() - stats.original();
916 assert!((term - expected).abs() < 1e-9, "CM {cm}: term {term}");
917 if let Some(p) = previous {
918 assert!(term > p, "CM {cm}: {term} must exceed {p}");
919 }
920 previous = Some(term);
921 }
922 }
923
924 #[test]
928 fn mi_sei_equals_original_without_comments() {
929 let stats = ratio_stats(0.0);
930 assert_eq!(stats.sei(), stats.original());
931 assert!(stats.original() > 0.0);
932 }
933
934 #[cfg(feature = "rust")]
935 #[test]
936 fn rust_mi_smoke() {
937 check_metrics::<RustParser>("fn f() -> i32 { 1 }\n", "foo.rs", |metric| {
942 assert_comment_free_mi(
947 &metric,
948 (7, 7, 2, 1),
949 155.053_605_022_880_67,
950 90.674_622_820_398_06,
951 );
952 });
953 }
954
955 #[cfg(feature = "go")]
956 #[test]
957 fn go_mi_smoke() {
958 check_metrics::<GoParser>(
962 "package main\nfunc f() int { return 1 }\n",
963 "foo.go",
964 |metric| {
965 assert_comment_free_mi(
968 &metric,
969 (9, 9, 2, 2),
970 141.886_166_643_237_35,
971 82.974_366_458_033_53,
972 );
973 },
974 );
975 }
976
977 #[cfg(feature = "elixir")]
978 #[test]
979 fn elixir_mi_smoke() {
980 check_metrics::<ElixirParser>(
984 "defmodule Foo do\n def f(x), do: x + 1\nend\n",
985 "foo.ex",
986 |metric| {
987 assert_comment_free_mi(
990 &metric,
991 (13, 12, 3, 3),
992 132.535_654_592_252_29,
993 77.506_230_755_703_09,
994 );
995 },
996 );
997 }
998
999 #[cfg(feature = "cpp")]
1000 #[test]
1001 fn cpp_mi_smoke() {
1002 check_metrics::<CppParser>(
1007 "int f(int x) { if (x > 0) return 1; return 0; }",
1008 "foo.cpp",
1009 |metric| {
1010 assert_comment_free_mi(
1013 &metric,
1014 (17, 11, 3, 1),
1015 149.123_548_232_679_84,
1016 87.206_753_352_444_35,
1017 );
1018 },
1019 );
1020 }
1021
1022 #[cfg(feature = "javascript")]
1023 #[test]
1024 fn javascript_mi_smoke() {
1025 check_metrics::<JavascriptParser>(
1029 "function f(x) { if (x > 0) return 1; return 0; }",
1030 "foo.js",
1031 |metric| {
1032 assert_comment_free_mi(
1035 &metric,
1036 (16, 11, 3, 1),
1037 149.438_796_266_125_3,
1038 87.391_108_927_558_66,
1039 );
1040 },
1041 );
1042 }
1043
1044 #[cfg(feature = "mozjs")]
1045 #[test]
1046 fn mozjs_mi_smoke() {
1047 check_metrics::<MozjsParser>(
1049 "function f(x) { if (x > 0) return 1; return 0; }",
1050 "foo.js",
1051 |metric| {
1052 assert_comment_free_mi(
1054 &metric,
1055 (16, 11, 3, 1),
1056 149.438_796_266_125_3,
1057 87.391_108_927_558_66,
1058 );
1059 },
1060 );
1061 }
1062
1063 #[cfg(feature = "python")]
1070 #[test]
1071 fn mi_comment_ratio_clamped() {
1072 let loc = loc::Stats::with_cloc_sloc(2, 0);
1073 assert_eq!(loc.cloc(), 2);
1074 assert_eq!(loc.sloc(), 1);
1075
1076 let cyclomatic = cyclomatic::Stats::default();
1077 let halstead = halstead::Stats::default();
1078 let mut mi = Stats::default();
1079 PythonCode::compute(&loc, &cyclomatic, &halstead, &mut mi, false);
1082
1083 assert_eq!(mi.comment_ratio, 1.0, "comment_ratio must clamp to 1");
1084 }
1085
1086 #[cfg(feature = "python")]
1089 #[test]
1090 fn mi_comment_ratio_is_zero_without_rows() {
1091 let loc = loc::Stats::default();
1092 assert_eq!(loc.sloc(), 0);
1093 for charges_docs in [false, true] {
1094 let mut mi = Stats::default();
1095 PythonCode::compute(
1096 &loc,
1097 &cyclomatic::Stats::default(),
1098 &halstead::Stats::default(),
1099 &mut mi,
1100 charges_docs,
1101 );
1102 assert_eq!(mi.comment_ratio.to_bits(), 0.0_f64.to_bits());
1103 }
1104 }
1105}