1use serde::{Deserialize, Serialize, Serializer};
50
51use crate::metric_set::{Metric, MetricSet};
52use crate::metrics::{
53 abc, cognitive, cyclomatic, halstead, loc, mi, nargs, nexits, nom, npa, npm, tokens, wmc,
54};
55use crate::spaces::SpaceKind;
56use crate::suppression::SuppressionScope;
57use crate::{function, ops};
58
59mod metrics;
65mod ops_view;
68#[cfg(feature = "vcs-git")]
71mod vcs;
72
73pub use metrics::*;
74#[cfg(feature = "vcs-git")]
75pub use vcs::*;
76
77fn nan_default() -> f64 {
81 f64::NAN
82}
83
84mod non_finite {
94 use serde::{Deserialize, Deserializer, Serializer};
95
96 #[allow(clippy::trivially_copy_pass_by_ref)]
99 pub(super) fn serialize<S: Serializer>(value: &f64, serializer: S) -> Result<S::Ok, S::Error> {
100 if value.is_finite() {
101 serializer.serialize_f64(*value)
102 } else {
103 serializer.serialize_none()
104 }
105 }
106
107 pub(super) fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<f64, D::Error> {
108 Ok(Option::<f64>::deserialize(deserializer)?.unwrap_or(f64::NAN))
109 }
110}
111
112#[cfg(feature = "vcs-git")]
116fn latest_present_risk(points: &[Option<crate::vcs::Stats>]) -> f64 {
117 points
118 .iter()
119 .rev()
120 .find_map(|s| s.as_ref().map(|s| s.risk_score))
121 .unwrap_or(0.0)
122}
123
124#[cfg(all(test, feature = "vcs-git"))]
125mod trend_wire_tests {
126 use super::*;
127
128 #[allow(clippy::float_cmp)]
131 fn risk(points: &[Option<f64>]) -> f64 {
132 let owned: Vec<Option<crate::vcs::Stats>> = points
133 .iter()
134 .map(|p| {
135 p.map(|risk_score| crate::vcs::Stats {
136 risk_score,
137 ..Default::default()
138 })
139 })
140 .collect();
141 latest_present_risk(&owned)
142 }
143
144 #[test]
145 #[allow(clippy::float_cmp)]
146 fn latest_present_risk_picks_the_newest_present_point() {
147 assert_eq!(risk(&[Some(1.0), None, Some(3.0), None]), 3.0);
150 }
151
152 #[test]
153 #[allow(clippy::float_cmp)]
154 fn latest_present_risk_defaults_to_zero_when_all_absent() {
155 assert_eq!(risk(&[None, None]), 0.0);
158 assert_eq!(risk(&[]), 0.0);
159 }
160
161 fn sample_vcs() -> Vcs {
163 Vcs {
164 commits_long: 12,
165 commits_recent: 4,
166 churn_long: 340,
167 churn_recent: 90,
168 authors_long: 3,
169 authors_recent: 2,
170 ownership_top_share: 0.625,
171 burst: 0.333,
172 bug_fix_commits: 2,
173 security_fix_commits: 1,
174 revert_commits: 0,
175 age_days: 200,
176 last_modified_days: 5,
177 change_entropy_long: 1.5,
178 change_entropy_recent: 0.5,
179 cochange_entropy_long: 2.0,
180 cochange_entropy_recent: 0.25,
181 risk_score: 7.5,
182 hotspot_score: Some(3.25),
183 author_ids: Some(vec!["deadbeef".to_owned()]),
184 }
185 }
186
187 #[test]
192 fn vcs_non_finite_floats_round_trip_as_null() {
193 let mut row = sample_vcs();
194 row.risk_score = f64::NAN;
195 row.burst = f64::INFINITY;
196 row.cochange_entropy_recent = f64::NEG_INFINITY;
197
198 let json = serde_json::to_string(&row).expect("serialize Vcs with NaN to JSON");
201 assert!(json.contains("\"risk_score\":null"), "got {json}");
202 let from_json: Vcs = serde_json::from_str(&json).expect("parse Vcs from JSON");
203 assert!(from_json.risk_score.is_nan());
204 assert!(from_json.burst.is_nan());
205 assert!(from_json.cochange_entropy_recent.is_nan());
206 assert_eq!(from_json.commits_long, row.commits_long);
208 assert!((from_json.ownership_top_share - row.ownership_top_share).abs() < 1e-12);
209
210 let yaml = serde_yaml::to_string(&row).expect("serialize Vcs to YAML");
212 let from_yaml: Vcs = serde_yaml::from_str(&yaml).expect("parse Vcs from YAML");
213 assert!(from_yaml.risk_score.is_nan() && from_yaml.burst.is_nan());
214
215 let mut bytes = Vec::new();
217 ciborium::into_writer(&row, &mut bytes).expect("serialize Vcs to CBOR");
218 let from_cbor: Vcs = ciborium::from_reader(bytes.as_slice()).expect("parse Vcs from CBOR");
219 assert!(from_cbor.risk_score.is_nan() && from_cbor.cochange_entropy_recent.is_nan());
220 }
221
222 #[test]
227 fn vcs_trend_point_round_trips_through_yaml_and_cbor() {
228 let point = VcsTrendPoint {
229 as_of: 1_700_000_000,
230 vcs: sample_vcs(),
231 };
232
233 let yaml = serde_yaml::to_string(&point).expect("serialize VcsTrendPoint to YAML");
234 let from_yaml: VcsTrendPoint = serde_yaml::from_str(&yaml).expect("parse point from YAML");
235 assert_eq!(from_yaml, point);
236
237 let mut bytes = Vec::new();
238 ciborium::into_writer(&point, &mut bytes).expect("serialize VcsTrendPoint to CBOR");
239 let from_cbor: VcsTrendPoint =
240 ciborium::from_reader(bytes.as_slice()).expect("parse point from CBOR");
241 assert_eq!(from_cbor, point);
242 }
243
244 #[test]
245 fn vcs_trend_round_trips_through_yaml_and_cbor() {
246 let trend = VcsTrend {
247 trend_schema_version: 1,
248 vcs_schema_version: 2,
249 risk_score_version: 2,
250 long_window_days: 365,
251 recent_window_days: 90,
252 truncated_shallow_clone: false,
253 as_of_points: vec![1_699_000_000, 1_700_000_000],
254 files: std::collections::BTreeMap::from([(
255 "src/lib.rs".to_owned(),
256 vec![
257 None,
258 Some(VcsTrendPoint {
259 as_of: 1_700_000_000,
260 vcs: sample_vcs(),
261 }),
262 ],
263 )]),
264 deltas: VcsTrendDeltas::default(),
265 };
266
267 let yaml = serde_yaml::to_string(&trend).expect("serialize VcsTrend to YAML");
268 let from_yaml: VcsTrend = serde_yaml::from_str(&yaml).expect("parse VcsTrend from YAML");
269 assert_eq!(from_yaml, trend);
270
271 let mut bytes = Vec::new();
272 ciborium::into_writer(&trend, &mut bytes).expect("serialize VcsTrend to CBOR");
273 let from_cbor: VcsTrend =
274 ciborium::from_reader(bytes.as_slice()).expect("parse VcsTrend from CBOR");
275 assert_eq!(from_cbor, trend);
276 }
277}
278
279#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
289pub struct CodeMetrics {
290 #[serde(default, skip_serializing_if = "Option::is_none")]
292 pub nargs: Option<Nargs>,
293 #[serde(default, skip_serializing_if = "Option::is_none")]
295 pub nexits: Option<Nexits>,
296 #[serde(default, skip_serializing_if = "Option::is_none")]
298 pub cognitive: Option<Cognitive>,
299 #[serde(default, skip_serializing_if = "Option::is_none")]
301 pub cyclomatic: Option<Cyclomatic>,
302 #[serde(default, skip_serializing_if = "Option::is_none")]
304 pub halstead: Option<Halstead>,
305 #[serde(default, skip_serializing_if = "Option::is_none")]
307 pub loc: Option<Loc>,
308 #[serde(default, skip_serializing_if = "Option::is_none")]
310 pub nom: Option<Nom>,
311 #[serde(default, skip_serializing_if = "Option::is_none")]
313 pub tokens: Option<Tokens>,
314 #[serde(default, skip_serializing_if = "Option::is_none")]
316 pub mi: Option<Mi>,
317 #[serde(default, skip_serializing_if = "Option::is_none")]
319 pub abc: Option<Abc>,
320 #[serde(default, skip_serializing_if = "Option::is_none")]
322 pub wmc: Option<Wmc>,
323 #[serde(default, skip_serializing_if = "Option::is_none")]
325 pub npm: Option<Npm>,
326 #[serde(default, skip_serializing_if = "Option::is_none")]
328 pub npa: Option<Npa>,
329 #[cfg(feature = "vcs-git")]
332 #[serde(default, skip_serializing_if = "Option::is_none")]
333 pub vcs: Option<Vcs>,
334}
335
336impl From<&crate::spaces::CodeMetrics> for CodeMetrics {
337 fn from(c: &crate::spaces::CodeMetrics) -> Self {
338 let sel = c.selected;
339 let on = |m: Metric| sel.contains(m);
343 Self {
344 nargs: on(Metric::Nargs).then(|| Nargs::from(&c.nargs)),
345 nexits: on(Metric::Nexits).then(|| Nexits::from(&c.nexits)),
346 cognitive: on(Metric::Cognitive).then(|| Cognitive::from(&c.cognitive)),
347 cyclomatic: on(Metric::Cyclomatic).then(|| Cyclomatic::from(&c.cyclomatic)),
348 halstead: on(Metric::Halstead).then(|| Halstead::from(&c.halstead)),
349 loc: on(Metric::Loc).then(|| Loc::from(&c.loc)),
350 nom: on(Metric::Nom).then(|| Nom::from(&c.nom)),
351 tokens: on(Metric::Tokens).then(|| Tokens::from(&c.tokens)),
352 mi: on(Metric::Mi).then(|| Mi::from(&c.mi)),
353 abc: on(Metric::Abc).then(|| Abc::from(&c.abc)),
354 wmc: (on(Metric::Wmc) && !c.wmc.is_disabled()).then(|| Wmc::from(&c.wmc)),
355 npm: (on(Metric::Npm) && !c.npm.is_disabled()).then(|| Npm::from(&c.npm)),
356 npa: (on(Metric::Npa) && !c.npa.is_disabled()).then(|| Npa::from(&c.npa)),
357 #[cfg(feature = "vcs-git")]
360 vcs: c.vcs.as_ref().map(Vcs::from),
361 }
362 }
363}
364
365impl CodeMetrics {
366 #[must_use]
373 pub fn selected(&self) -> MetricSet {
374 let mut set = MetricSet::empty();
375 let mut mark = |present: bool, metric: Metric| {
376 if present {
377 set.insert(metric);
378 }
379 };
380 mark(self.nargs.is_some(), Metric::Nargs);
381 mark(self.nexits.is_some(), Metric::Nexits);
382 mark(self.cognitive.is_some(), Metric::Cognitive);
383 mark(self.cyclomatic.is_some(), Metric::Cyclomatic);
384 mark(self.halstead.is_some(), Metric::Halstead);
385 mark(self.loc.is_some(), Metric::Loc);
386 mark(self.nom.is_some(), Metric::Nom);
387 mark(self.tokens.is_some(), Metric::Tokens);
388 mark(self.mi.is_some(), Metric::Mi);
389 mark(self.abc.is_some(), Metric::Abc);
390 mark(self.wmc.is_some(), Metric::Wmc);
391 mark(self.npm.is_some(), Metric::Npm);
392 mark(self.npa.is_some(), Metric::Npa);
393 set
394 }
395}
396
397pub const MAX_SPACE_SERIALIZE_DEPTH: usize = 128;
414
415fn map_tree<'a, Src, Dst>(
425 root: &'a Src,
426 children_of: fn(&'a Src) -> &'a [Src],
427 build: fn(&'a Src, Vec<Dst>) -> Dst,
428) -> Dst {
429 let mut root_frame = MapFrame::new(root, children_of);
433 let mut descendants = Vec::new();
434 loop {
435 let frame = match descendants.last_mut() {
436 Some(frame) => frame,
437 None => &mut root_frame,
438 };
439 let source = frame.source;
440 if let Some(child) = children_of(source).get(frame.next_child) {
441 frame.next_child += 1;
442 descendants.push(MapFrame::new(child, children_of));
443 continue;
444 }
445 let Some(done) = descendants.pop() else { break };
448 let converted = build(done.source, done.children);
449 match descendants.last_mut() {
450 Some(parent) => parent.children.push(converted),
451 None => root_frame.children.push(converted),
452 }
453 }
454 build(root_frame.source, root_frame.children)
455}
456
457struct MapFrame<'a, Src, Dst> {
459 source: &'a Src,
461 next_child: usize,
463 children: Vec<Dst>,
465}
466
467impl<'a, Src, Dst> MapFrame<'a, Src, Dst> {
468 fn new(source: &'a Src, children_of: fn(&'a Src) -> &'a [Src]) -> Self {
469 Self {
470 source,
471 next_child: 0,
472 children: Vec::with_capacity(children_of(source).len()),
473 }
474 }
475}
476
477fn serialize_spaces<S: Serializer>(spaces: &[FuncSpace], serializer: S) -> Result<S::Ok, S::Error> {
480 crate::recursion::serialize_bounded(spaces, MAX_SPACE_SERIALIZE_DEPTH, "FuncSpace", serializer)
481}
482
483fn serialize_ops_spaces<S: Serializer>(spaces: &[Ops], serializer: S) -> Result<S::Ok, S::Error> {
486 crate::recursion::serialize_bounded(spaces, MAX_SPACE_SERIALIZE_DEPTH, "Ops", serializer)
487}
488
489#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
491pub struct FuncSpace {
492 pub name: Option<String>,
494 pub start_line: usize,
496 pub end_line: usize,
498 pub kind: SpaceKind,
500 #[serde(serialize_with = "serialize_spaces")]
502 pub spaces: Vec<FuncSpace>,
503 pub metrics: CodeMetrics,
505 #[serde(default, skip_serializing_if = "SuppressionScope::is_empty")]
508 pub suppressed: SuppressionScope,
509}
510
511crate::recursion::impl_iterative_drop!(FuncSpace, spaces);
514
515impl From<&crate::spaces::FuncSpace> for FuncSpace {
516 fn from(f: &crate::spaces::FuncSpace) -> Self {
517 map_tree(
518 f,
519 |source| &source.spaces,
520 |source, spaces| Self {
521 name: source.name.clone(),
522 start_line: source.start_line,
523 end_line: source.end_line,
524 kind: source.kind,
525 spaces,
526 metrics: CodeMetrics::from(&source.metrics),
527 suppressed: source.suppressed.clone(),
528 },
529 )
530 }
531}
532
533#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
535pub struct Ops {
536 pub name: Option<String>,
538 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
540 pub name_was_lossy: bool,
541 pub start_line: usize,
543 pub end_line: usize,
545 pub kind: SpaceKind,
547 #[serde(serialize_with = "serialize_ops_spaces")]
549 pub spaces: Vec<Ops>,
550 pub operands: Vec<String>,
552 pub operators: Vec<String>,
554}
555
556crate::recursion::impl_iterative_drop!(Ops, spaces);
559
560crate::observation::counter!(owned_ops_projections);
567
568impl From<&ops::Ops> for Ops {
569 fn from(o: &ops::Ops) -> Self {
570 owned_ops_projections::record();
571 map_tree(
572 o,
573 |source| &source.spaces,
574 |source, spaces| Self {
575 name: source.name.clone(),
576 name_was_lossy: source.name_was_lossy,
577 start_line: source.start_line,
578 end_line: source.end_line,
579 kind: source.kind,
580 spaces,
581 operands: source.operands.clone(),
582 operators: source.operators.clone(),
583 },
584 )
585 }
586}
587
588#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
590pub struct FunctionSpan {
591 pub name: Option<String>,
593 pub start_line: usize,
595 pub end_line: usize,
597}
598
599impl From<&function::FunctionSpan> for FunctionSpan {
600 fn from(f: &function::FunctionSpan) -> Self {
601 Self {
602 name: f.name.clone(),
603 start_line: f.start_line,
604 end_line: f.end_line,
605 }
606 }
607}
608
609macro_rules! serialize_via_wire {
619 ($compute:ty => $wire:ident) => {
620 impl Serialize for $compute {
621 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
622 $wire::from(self).serialize(serializer)
623 }
624 }
625 };
626}
627
628serialize_via_wire!(abc::Stats => Abc);
629serialize_via_wire!(cognitive::Stats => Cognitive);
630serialize_via_wire!(cyclomatic::Stats => Cyclomatic);
631serialize_via_wire!(nexits::Stats => Nexits);
632serialize_via_wire!(halstead::Stats => Halstead);
633serialize_via_wire!(loc::Stats => Loc);
634serialize_via_wire!(mi::Stats => Mi);
635serialize_via_wire!(nargs::Stats => Nargs);
636serialize_via_wire!(nom::Stats => Nom);
637serialize_via_wire!(npa::Stats => Npa);
638serialize_via_wire!(npm::Stats => Npm);
639serialize_via_wire!(tokens::Stats => Tokens);
640serialize_via_wire!(wmc::Stats => Wmc);
641serialize_via_wire!(crate::spaces::CodeMetrics => CodeMetrics);
642serialize_via_wire!(crate::spaces::FuncSpace => FuncSpace);
643serialize_via_wire!(function::FunctionSpan => FunctionSpan);
646
647#[cfg(test)]
650#[path = "wire_ops_tests.rs"]
651mod ops_tests;
652
653#[cfg(test)]
654#[allow(clippy::float_cmp)]
660mod tests {
661 use super::*;
662 use crate::RustParser;
663 use crate::test_support::check_func_space;
664
665 #[cfg(feature = "rust")]
668 const FIXTURE: &str = "\
669fn classify(x: i32) -> i32 {
670 if x > 0 {
671 x * 2
672 } else if x < 0 {
673 -x
674 } else {
675 0
676 }
677}
678
679fn run() {
680 let adder = |a: i32, b: i32| a + b;
681 let _ = adder(classify(3), classify(-4));
682}
683";
684
685 #[cfg(feature = "rust")]
692 fn assert_fixture_oracle(tree: &FuncSpace) {
693 assert_eq!(tree.kind, SpaceKind::Unit);
695 assert_eq!(tree.spaces.len(), 2, "classify + run");
696
697 let m = &tree.metrics;
698 assert_eq!(m.cyclomatic.as_ref().unwrap().sum, 6, "unit cyclomatic.sum");
699 assert_eq!(
703 m.cyclomatic.as_ref().unwrap().value,
704 1,
705 "unit cyclomatic.value (own, excludes children)"
706 );
707 assert_eq!(m.cognitive.as_ref().unwrap().sum, 3, "unit cognitive.sum");
708 assert_eq!(
709 m.cognitive.as_ref().unwrap().value,
710 0,
711 "unit cognitive.value (own)"
712 );
713 assert_eq!(m.loc.as_ref().unwrap().sloc, 14, "unit loc.sloc");
714 assert_eq!(m.nom.as_ref().unwrap().total, 3, "unit nom.total");
715 let abc = m.abc.as_ref().unwrap();
718 assert_eq!((abc.assignments, abc.branches, abc.conditions), (2, 3, 4));
719
720 let classify = tree
721 .spaces
722 .iter()
723 .find(|s| s.name.as_deref() == Some("classify"))
724 .expect("classify space");
725 let classify_cyclo = classify.metrics.cyclomatic.as_ref().unwrap();
726 assert_eq!(classify_cyclo.sum, 3, "classify cyclomatic.sum");
727 assert_eq!(classify_cyclo.value, 3, "classify cyclomatic.value (leaf)");
729
730 let run = tree
734 .spaces
735 .iter()
736 .find(|s| s.name.as_deref() == Some("run"))
737 .expect("run space");
738 let run_cyclo = run.metrics.cyclomatic.as_ref().unwrap();
739 assert_eq!(run_cyclo.sum, 2, "run cyclomatic.sum (run + adder closure)");
740 assert_eq!(
741 run_cyclo.value, 1,
742 "run cyclomatic.value (own, excludes closure)"
743 );
744 }
745
746 #[cfg(feature = "rust")]
751 #[test]
752 fn json_round_trips() {
753 check_func_space::<RustParser, _>(FIXTURE, "fixture.rs", |fs| {
754 let json = serde_json::to_string(&fs).expect("serialize FuncSpace to JSON");
755 let back: FuncSpace = serde_json::from_str(&json).expect("parse wire::FuncSpace");
756 assert_eq!(
757 back,
758 fs.to_wire(),
759 "deserialized wire tree must equal the projection"
760 );
761 assert_eq!(
762 serde_json::to_string(&back).expect("re-serialize wire"),
763 json,
764 "re-serialized wire must be byte-identical to the original JSON",
765 );
766 assert_fixture_oracle(&back);
769 });
770 }
771
772 #[cfg(feature = "rust")]
773 #[test]
774 fn yaml_round_trips() {
775 check_func_space::<RustParser, _>(FIXTURE, "fixture.rs", |fs| {
776 let yaml = serde_yaml::to_string(&fs).expect("serialize to YAML");
777 let back: FuncSpace = serde_yaml::from_str(&yaml).expect("parse wire from YAML");
778 assert_eq!(back, fs.to_wire());
779 assert_eq!(serde_yaml::to_string(&back).expect("re-serialize"), yaml);
780 });
781 }
782
783 #[cfg(feature = "rust")]
784 #[test]
785 fn toml_round_trips() {
786 check_func_space::<RustParser, _>(FIXTURE, "fixture.rs", |fs| {
787 let toml = toml::to_string(&fs).expect("serialize to TOML");
788 let back: FuncSpace = toml::from_str(&toml).expect("parse wire from TOML");
789 assert_eq!(back, fs.to_wire());
790 assert_eq!(toml::to_string(&back).expect("re-serialize"), toml);
791 });
792 }
793
794 #[cfg(feature = "rust")]
795 #[test]
796 fn cbor_round_trips() {
797 check_func_space::<RustParser, _>(FIXTURE, "fixture.rs", |fs| {
798 let mut bytes = Vec::new();
799 ciborium::into_writer(&fs, &mut bytes).expect("serialize to CBOR");
800 let back: FuncSpace =
801 ciborium::from_reader(bytes.as_slice()).expect("parse wire from CBOR");
802 assert_eq!(back, fs.to_wire());
803 let mut re = Vec::new();
804 ciborium::into_writer(&back, &mut re).expect("re-serialize");
805 assert_eq!(re, bytes, "CBOR re-serialization must be byte-identical");
806 });
807 }
808
809 #[test]
814 fn function_span_round_trips() {
815 let resolved = FunctionSpan {
816 name: Some("foo".to_owned()),
817 start_line: 1,
818 end_line: 4,
819 };
820 let unresolved = FunctionSpan {
821 name: None,
822 start_line: 7,
823 end_line: 8,
824 };
825
826 for span in [resolved, unresolved] {
827 let json = serde_json::to_string(&span).expect("serialize FunctionSpan");
828 assert!(
829 !json.contains("error"),
830 "FunctionSpan JSON must not carry an `error` key, got {json}",
831 );
832 let back: FunctionSpan = serde_json::from_str(&json).expect("parse FunctionSpan");
833 assert_eq!(back, span, "FunctionSpan must round-trip through JSON");
834 }
835
836 let json = serde_json::to_string(&FunctionSpan {
838 name: None,
839 start_line: 7,
840 end_line: 8,
841 })
842 .expect("serialize");
843 assert!(
844 json.contains(r#""name":null"#),
845 "unresolved name must serialize to JSON null, got {json}",
846 );
847 }
848
849 #[test]
854 fn non_finite_floats_round_trip_as_null_or_omission() {
855 for probe in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
856 let mi = Mi {
857 original: probe,
858 sei: 1.5,
859 visual_studio: 2.0,
860 };
861
862 let json = serde_json::to_string(&mi).expect("JSON");
863 assert!(
864 json.contains(r#""original":null"#),
865 "non-finite must serialize to JSON null, got {json}",
866 );
867 assert!(
868 serde_json::from_str::<Mi>(&json)
869 .expect("parse")
870 .original
871 .is_nan(),
872 "JSON null must deserialize back to NaN",
873 );
874
875 let yaml = serde_yaml::to_string(&mi).expect("YAML");
876 assert!(
877 yaml.contains("original: null"),
878 "non-finite must serialize to YAML null, got {yaml}",
879 );
880 assert!(
881 serde_yaml::from_str::<Mi>(&yaml)
882 .expect("parse")
883 .original
884 .is_nan()
885 );
886
887 let toml = toml::to_string(&mi).expect("TOML");
888 assert!(
889 !toml.contains("original"),
890 "TOML must omit the non-finite key (no null literal), got {toml}",
891 );
892 assert!(
893 toml::from_str::<Mi>(&toml)
894 .expect("parse")
895 .original
896 .is_nan(),
897 "omitted TOML key must default back to NaN",
898 );
899
900 let mut cbor = Vec::new();
903 ciborium::into_writer(&mi, &mut cbor).expect("CBOR");
904 let value: ciborium::value::Value =
905 ciborium::from_reader(cbor.as_slice()).expect("parse cbor value");
906 let ciborium::value::Value::Map(map) = &value else {
907 panic!("CBOR root is not a map");
908 };
909 let original_key = ciborium::value::Value::Text("original".to_owned());
910 let original = map
911 .iter()
912 .find_map(|(k, v)| (*k == original_key).then_some(v));
913 assert_eq!(
914 original,
915 Some(&ciborium::value::Value::Null),
916 "non-finite must serialize to CBOR null",
917 );
918 assert!(
919 ciborium::from_reader::<Mi, _>(cbor.as_slice())
920 .expect("parse")
921 .original
922 .is_nan(),
923 "CBOR null must deserialize back to NaN",
924 );
925
926 let back = serde_json::from_str::<Mi>(&json).expect("parse");
928 assert_eq!(back.sei, 1.5);
929 assert_eq!(back.visual_studio, 2.0);
930 }
931 }
932
933 #[cfg(feature = "rust")]
937 #[test]
938 fn selected_is_inferred_from_present_keys() {
939 check_func_space::<RustParser, _>(FIXTURE, "fixture.rs", |fs| {
940 let full = fs.metrics.to_wire();
941 let selected = full.selected();
942 assert!(selected.contains(Metric::Loc));
943 assert!(selected.contains(Metric::Cyclomatic));
944
945 let json = serde_json::to_string(&full).expect("serialize metrics");
947 let mut value: serde_json::Value = serde_json::from_str(&json).expect("parse value");
948 let obj = value.as_object_mut().expect("metrics object");
949 obj.retain(|k, _| k == "loc");
950 let pruned: CodeMetrics =
951 serde_json::from_value(value).expect("parse pruned wire metrics");
952 let pruned_selected = pruned.selected();
953 assert!(pruned_selected.contains(Metric::Loc));
954 assert!(!pruned_selected.contains(Metric::Cyclomatic));
955 assert!(pruned.cyclomatic.is_none());
956 });
957 }
958
959 #[cfg(feature = "rust")]
973 const PRODUCTION_STACK: usize = 2 * 1024 * 1024;
974
975 const TIGHT_STACK: usize = 512 * 1024;
979
980 pub(super) fn nested_functions(depth: usize) -> String {
983 use std::fmt::Write as _;
984 let mut source = String::with_capacity(depth * 14);
985 for level in 0..depth {
986 let _ = writeln!(source, "fn f{level}() {{");
987 }
988 for _ in 0..depth {
989 source.push_str("}\n");
990 }
991 source
992 }
993
994 #[cfg(feature = "rust")]
997 fn analyze_nested(depth: usize) -> crate::FuncSpace {
998 crate::analyze(
999 crate::Source::new(crate::LANG::Rust, nested_functions(depth).as_bytes())
1000 .with_name(Some("nested.rs".to_owned())),
1001 crate::MetricsOptions::default().with_only(&[Metric::Loc]),
1002 )
1003 .expect("nested-function fixture must analyse")
1004 }
1005
1006 fn wire_nesting_depth(space: &FuncSpace) -> usize {
1010 let mut deepest = 0;
1011 let mut stack = vec![(space, 1_usize)];
1012 while let Some((node, depth)) = stack.pop() {
1013 deepest = deepest.max(depth);
1014 for child in &node.spaces {
1015 stack.push((child, depth + 1));
1016 }
1017 }
1018 deepest
1019 }
1020
1021 fn on_stack<T: Send + 'static>(bytes: usize, body: impl FnOnce() -> T + Send + 'static) -> T {
1024 std::thread::Builder::new()
1025 .stack_size(bytes)
1026 .spawn(body)
1027 .expect("spawn bounded-stack thread")
1028 .join()
1029 .expect("bounded-stack thread must not overflow")
1030 }
1031
1032 fn space_chain(depth: usize) -> crate::FuncSpace {
1040 let leaf = || crate::FuncSpace {
1041 name: Some("f".to_owned()),
1042 start_line: 1,
1043 end_line: 1,
1044 kind: SpaceKind::Function,
1045 spaces: Vec::new(),
1046 metrics: crate::CodeMetrics::default(),
1047 suppressed: SuppressionScope::default(),
1048 };
1049 let mut root = leaf();
1050 let mut cursor = &mut root;
1051 for _ in 0..depth {
1052 cursor.spaces.push(leaf());
1053 cursor = cursor.spaces.last_mut().expect("just pushed");
1054 }
1055 root
1056 }
1057
1058 #[cfg(feature = "rust")]
1059 #[test]
1060 fn deeply_nested_spaces_convert_to_wire_without_stack_overflow() {
1061 const DEPTH: usize = 2_000;
1067 let depth = on_stack(TIGHT_STACK, || {
1068 let space = analyze_nested(DEPTH);
1069 wire_nesting_depth(&space.to_wire())
1070 });
1071 assert_eq!(depth, DEPTH + 1, "the whole chain must survive conversion");
1073 }
1074
1075 #[test]
1076 fn a_pathologically_deep_space_chain_converts_and_tears_down() {
1077 const DEPTH: usize = 100_000;
1082 let depth = on_stack(TIGHT_STACK, || {
1083 let space = space_chain(DEPTH);
1084 wire_nesting_depth(&space.to_wire())
1085 });
1086 assert_eq!(depth, DEPTH + 1, "the whole chain must survive conversion");
1087 }
1088
1089 #[cfg(feature = "rust")]
1090 #[test]
1091 fn spaces_deeper_than_the_limit_fail_serialization_rather_than_abort() {
1092 const DEPTH: usize = 2_000;
1097 let message = on_stack(PRODUCTION_STACK, || {
1098 let space = analyze_nested(DEPTH);
1099 serde_json::to_string(&space)
1100 .expect_err("nesting past the limit must fail, not serialize")
1101 .to_string()
1102 });
1103 assert!(
1104 message.contains("FuncSpace nesting is deeper than the serialization limit of 128"),
1105 "the error must name the type and the limit, got: {message}"
1106 );
1107 }
1108
1109 #[cfg(feature = "rust")]
1110 #[test]
1111 fn space_nesting_at_the_serialize_limit_is_accepted_and_one_deeper_is_not() {
1112 let (accepted, rejected) = on_stack(PRODUCTION_STACK, || {
1116 let at_limit = analyze_nested(MAX_SPACE_SERIALIZE_DEPTH);
1117 let past_limit = analyze_nested(MAX_SPACE_SERIALIZE_DEPTH + 1);
1118 (
1119 [
1120 serde_json::to_string(&at_limit).is_ok(),
1121 serde_yaml::to_string(&at_limit).is_ok(),
1122 toml::to_string(&at_limit).is_ok(),
1123 {
1124 let mut bytes = Vec::new();
1125 ciborium::into_writer(&at_limit, &mut bytes).is_ok()
1126 },
1127 ],
1128 [
1129 serde_json::to_string(&past_limit).is_ok(),
1130 serde_yaml::to_string(&past_limit).is_ok(),
1131 toml::to_string(&past_limit).is_ok(),
1132 {
1133 let mut bytes = Vec::new();
1134 ciborium::into_writer(&past_limit, &mut bytes).is_ok()
1135 },
1136 ],
1137 )
1138 });
1139 assert_eq!(
1140 accepted, [true; 4],
1141 "exactly {MAX_SPACE_SERIALIZE_DEPTH} levels must serialize in every format"
1142 );
1143 assert_eq!(
1144 rejected, [false; 4],
1145 "one level past the limit must be refused in every format"
1146 );
1147 }
1148
1149 #[cfg(feature = "rust")]
1150 #[test]
1151 fn deeply_nested_ops_convert_and_serialize_without_stack_overflow() {
1152 const DEPTH: usize = 2_000;
1155 let (converted_depth, message) = on_stack(PRODUCTION_STACK, || {
1156 let ops = crate::Ast::parse(crate::Source::new(
1157 crate::LANG::Rust,
1158 nested_functions(DEPTH).as_bytes(),
1159 ))
1160 .expect("nested-function fixture must parse")
1161 .ops()
1162 .expect("nested-function fixture must yield ops");
1163 let wire = Ops::from(&ops);
1164 let mut deepest = 0;
1165 let mut stack = vec![(&wire, 1_usize)];
1166 while let Some((node, depth)) = stack.pop() {
1167 deepest = deepest.max(depth);
1168 for child in &node.spaces {
1169 stack.push((child, depth + 1));
1170 }
1171 }
1172 let message = serde_json::to_string(&ops)
1173 .expect_err("nesting past the limit must fail, not serialize")
1174 .to_string();
1175 (deepest, message)
1176 });
1177 assert_eq!(converted_depth, DEPTH + 1, "the whole chain must convert");
1178 assert!(
1179 message.contains("Ops nesting is deeper than the serialization limit of 128"),
1180 "the error must name the type and the limit, got: {message}"
1181 );
1182 }
1183}