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 const FIXTURE: &str = "\
668fn classify(x: i32) -> i32 {
669 if x > 0 {
670 x * 2
671 } else if x < 0 {
672 -x
673 } else {
674 0
675 }
676}
677
678fn run() {
679 let adder = |a: i32, b: i32| a + b;
680 let _ = adder(classify(3), classify(-4));
681}
682";
683
684 fn assert_fixture_oracle(tree: &FuncSpace) {
691 assert_eq!(tree.kind, SpaceKind::Unit);
693 assert_eq!(tree.spaces.len(), 2, "classify + run");
694
695 let m = &tree.metrics;
696 assert_eq!(m.cyclomatic.as_ref().unwrap().sum, 6, "unit cyclomatic.sum");
697 assert_eq!(
701 m.cyclomatic.as_ref().unwrap().value,
702 1,
703 "unit cyclomatic.value (own, excludes children)"
704 );
705 assert_eq!(m.cognitive.as_ref().unwrap().sum, 3, "unit cognitive.sum");
706 assert_eq!(
707 m.cognitive.as_ref().unwrap().value,
708 0,
709 "unit cognitive.value (own)"
710 );
711 assert_eq!(m.loc.as_ref().unwrap().sloc, 14, "unit loc.sloc");
712 assert_eq!(m.nom.as_ref().unwrap().total, 3, "unit nom.total");
713 let abc = m.abc.as_ref().unwrap();
716 assert_eq!((abc.assignments, abc.branches, abc.conditions), (2, 3, 4));
717
718 let classify = tree
719 .spaces
720 .iter()
721 .find(|s| s.name.as_deref() == Some("classify"))
722 .expect("classify space");
723 let classify_cyclo = classify.metrics.cyclomatic.as_ref().unwrap();
724 assert_eq!(classify_cyclo.sum, 3, "classify cyclomatic.sum");
725 assert_eq!(classify_cyclo.value, 3, "classify cyclomatic.value (leaf)");
727
728 let run = tree
732 .spaces
733 .iter()
734 .find(|s| s.name.as_deref() == Some("run"))
735 .expect("run space");
736 let run_cyclo = run.metrics.cyclomatic.as_ref().unwrap();
737 assert_eq!(run_cyclo.sum, 2, "run cyclomatic.sum (run + adder closure)");
738 assert_eq!(
739 run_cyclo.value, 1,
740 "run cyclomatic.value (own, excludes closure)"
741 );
742 }
743
744 #[test]
749 fn json_round_trips() {
750 check_func_space::<RustParser, _>(FIXTURE, "fixture.rs", |fs| {
751 let json = serde_json::to_string(&fs).expect("serialize FuncSpace to JSON");
752 let back: FuncSpace = serde_json::from_str(&json).expect("parse wire::FuncSpace");
753 assert_eq!(
754 back,
755 fs.to_wire(),
756 "deserialized wire tree must equal the projection"
757 );
758 assert_eq!(
759 serde_json::to_string(&back).expect("re-serialize wire"),
760 json,
761 "re-serialized wire must be byte-identical to the original JSON",
762 );
763 assert_fixture_oracle(&back);
766 });
767 }
768
769 #[test]
770 fn yaml_round_trips() {
771 check_func_space::<RustParser, _>(FIXTURE, "fixture.rs", |fs| {
772 let yaml = serde_yaml::to_string(&fs).expect("serialize to YAML");
773 let back: FuncSpace = serde_yaml::from_str(&yaml).expect("parse wire from YAML");
774 assert_eq!(back, fs.to_wire());
775 assert_eq!(serde_yaml::to_string(&back).expect("re-serialize"), yaml);
776 });
777 }
778
779 #[test]
780 fn toml_round_trips() {
781 check_func_space::<RustParser, _>(FIXTURE, "fixture.rs", |fs| {
782 let toml = toml::to_string(&fs).expect("serialize to TOML");
783 let back: FuncSpace = toml::from_str(&toml).expect("parse wire from TOML");
784 assert_eq!(back, fs.to_wire());
785 assert_eq!(toml::to_string(&back).expect("re-serialize"), toml);
786 });
787 }
788
789 #[test]
790 fn cbor_round_trips() {
791 check_func_space::<RustParser, _>(FIXTURE, "fixture.rs", |fs| {
792 let mut bytes = Vec::new();
793 ciborium::into_writer(&fs, &mut bytes).expect("serialize to CBOR");
794 let back: FuncSpace =
795 ciborium::from_reader(bytes.as_slice()).expect("parse wire from CBOR");
796 assert_eq!(back, fs.to_wire());
797 let mut re = Vec::new();
798 ciborium::into_writer(&back, &mut re).expect("re-serialize");
799 assert_eq!(re, bytes, "CBOR re-serialization must be byte-identical");
800 });
801 }
802
803 #[test]
808 fn function_span_round_trips() {
809 let resolved = FunctionSpan {
810 name: Some("foo".to_owned()),
811 start_line: 1,
812 end_line: 4,
813 };
814 let unresolved = FunctionSpan {
815 name: None,
816 start_line: 7,
817 end_line: 8,
818 };
819
820 for span in [resolved, unresolved] {
821 let json = serde_json::to_string(&span).expect("serialize FunctionSpan");
822 assert!(
823 !json.contains("error"),
824 "FunctionSpan JSON must not carry an `error` key, got {json}",
825 );
826 let back: FunctionSpan = serde_json::from_str(&json).expect("parse FunctionSpan");
827 assert_eq!(back, span, "FunctionSpan must round-trip through JSON");
828 }
829
830 let json = serde_json::to_string(&FunctionSpan {
832 name: None,
833 start_line: 7,
834 end_line: 8,
835 })
836 .expect("serialize");
837 assert!(
838 json.contains(r#""name":null"#),
839 "unresolved name must serialize to JSON null, got {json}",
840 );
841 }
842
843 #[test]
848 fn non_finite_floats_round_trip_as_null_or_omission() {
849 for probe in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
850 let mi = Mi {
851 original: probe,
852 sei: 1.5,
853 visual_studio: 2.0,
854 };
855
856 let json = serde_json::to_string(&mi).expect("JSON");
857 assert!(
858 json.contains(r#""original":null"#),
859 "non-finite must serialize to JSON null, got {json}",
860 );
861 assert!(
862 serde_json::from_str::<Mi>(&json)
863 .expect("parse")
864 .original
865 .is_nan(),
866 "JSON null must deserialize back to NaN",
867 );
868
869 let yaml = serde_yaml::to_string(&mi).expect("YAML");
870 assert!(
871 yaml.contains("original: null"),
872 "non-finite must serialize to YAML null, got {yaml}",
873 );
874 assert!(
875 serde_yaml::from_str::<Mi>(&yaml)
876 .expect("parse")
877 .original
878 .is_nan()
879 );
880
881 let toml = toml::to_string(&mi).expect("TOML");
882 assert!(
883 !toml.contains("original"),
884 "TOML must omit the non-finite key (no null literal), got {toml}",
885 );
886 assert!(
887 toml::from_str::<Mi>(&toml)
888 .expect("parse")
889 .original
890 .is_nan(),
891 "omitted TOML key must default back to NaN",
892 );
893
894 let mut cbor = Vec::new();
897 ciborium::into_writer(&mi, &mut cbor).expect("CBOR");
898 let value: ciborium::value::Value =
899 ciborium::from_reader(cbor.as_slice()).expect("parse cbor value");
900 let ciborium::value::Value::Map(map) = &value else {
901 panic!("CBOR root is not a map");
902 };
903 let original_key = ciborium::value::Value::Text("original".to_owned());
904 let original = map
905 .iter()
906 .find_map(|(k, v)| (*k == original_key).then_some(v));
907 assert_eq!(
908 original,
909 Some(&ciborium::value::Value::Null),
910 "non-finite must serialize to CBOR null",
911 );
912 assert!(
913 ciborium::from_reader::<Mi, _>(cbor.as_slice())
914 .expect("parse")
915 .original
916 .is_nan(),
917 "CBOR null must deserialize back to NaN",
918 );
919
920 let back = serde_json::from_str::<Mi>(&json).expect("parse");
922 assert_eq!(back.sei, 1.5);
923 assert_eq!(back.visual_studio, 2.0);
924 }
925 }
926
927 #[test]
931 fn selected_is_inferred_from_present_keys() {
932 check_func_space::<RustParser, _>(FIXTURE, "fixture.rs", |fs| {
933 let full = fs.metrics.to_wire();
934 let selected = full.selected();
935 assert!(selected.contains(Metric::Loc));
936 assert!(selected.contains(Metric::Cyclomatic));
937
938 let json = serde_json::to_string(&full).expect("serialize metrics");
940 let mut value: serde_json::Value = serde_json::from_str(&json).expect("parse value");
941 let obj = value.as_object_mut().expect("metrics object");
942 obj.retain(|k, _| k == "loc");
943 let pruned: CodeMetrics =
944 serde_json::from_value(value).expect("parse pruned wire metrics");
945 let pruned_selected = pruned.selected();
946 assert!(pruned_selected.contains(Metric::Loc));
947 assert!(!pruned_selected.contains(Metric::Cyclomatic));
948 assert!(pruned.cyclomatic.is_none());
949 });
950 }
951
952 const PRODUCTION_STACK: usize = 2 * 1024 * 1024;
966
967 const TIGHT_STACK: usize = 512 * 1024;
971
972 pub(super) fn nested_functions(depth: usize) -> String {
975 use std::fmt::Write as _;
976 let mut source = String::with_capacity(depth * 14);
977 for level in 0..depth {
978 let _ = writeln!(source, "fn f{level}() {{");
979 }
980 for _ in 0..depth {
981 source.push_str("}\n");
982 }
983 source
984 }
985
986 fn analyze_nested(depth: usize) -> crate::FuncSpace {
989 crate::analyze(
990 crate::Source::new(crate::LANG::Rust, nested_functions(depth).as_bytes())
991 .with_name(Some("nested.rs".to_owned())),
992 crate::MetricsOptions::default().with_only(&[Metric::Loc]),
993 )
994 .expect("nested-function fixture must analyse")
995 }
996
997 fn wire_nesting_depth(space: &FuncSpace) -> usize {
1001 let mut deepest = 0;
1002 let mut stack = vec![(space, 1_usize)];
1003 while let Some((node, depth)) = stack.pop() {
1004 deepest = deepest.max(depth);
1005 for child in &node.spaces {
1006 stack.push((child, depth + 1));
1007 }
1008 }
1009 deepest
1010 }
1011
1012 fn on_stack<T: Send + 'static>(bytes: usize, body: impl FnOnce() -> T + Send + 'static) -> T {
1015 std::thread::Builder::new()
1016 .stack_size(bytes)
1017 .spawn(body)
1018 .expect("spawn bounded-stack thread")
1019 .join()
1020 .expect("bounded-stack thread must not overflow")
1021 }
1022
1023 fn space_chain(depth: usize) -> crate::FuncSpace {
1031 let leaf = || crate::FuncSpace {
1032 name: Some("f".to_owned()),
1033 start_line: 1,
1034 end_line: 1,
1035 kind: SpaceKind::Function,
1036 spaces: Vec::new(),
1037 metrics: crate::CodeMetrics::default(),
1038 suppressed: SuppressionScope::default(),
1039 };
1040 let mut root = leaf();
1041 let mut cursor = &mut root;
1042 for _ in 0..depth {
1043 cursor.spaces.push(leaf());
1044 cursor = cursor.spaces.last_mut().expect("just pushed");
1045 }
1046 root
1047 }
1048
1049 #[test]
1050 fn deeply_nested_spaces_convert_to_wire_without_stack_overflow() {
1051 const DEPTH: usize = 2_000;
1057 let depth = on_stack(TIGHT_STACK, || {
1058 let space = analyze_nested(DEPTH);
1059 wire_nesting_depth(&space.to_wire())
1060 });
1061 assert_eq!(depth, DEPTH + 1, "the whole chain must survive conversion");
1063 }
1064
1065 #[test]
1066 fn a_pathologically_deep_space_chain_converts_and_tears_down() {
1067 const DEPTH: usize = 100_000;
1072 let depth = on_stack(TIGHT_STACK, || {
1073 let space = space_chain(DEPTH);
1074 wire_nesting_depth(&space.to_wire())
1075 });
1076 assert_eq!(depth, DEPTH + 1, "the whole chain must survive conversion");
1077 }
1078
1079 #[test]
1080 fn spaces_deeper_than_the_limit_fail_serialization_rather_than_abort() {
1081 const DEPTH: usize = 2_000;
1086 let message = on_stack(PRODUCTION_STACK, || {
1087 let space = analyze_nested(DEPTH);
1088 serde_json::to_string(&space)
1089 .expect_err("nesting past the limit must fail, not serialize")
1090 .to_string()
1091 });
1092 assert!(
1093 message.contains("FuncSpace nesting is deeper than the serialization limit of 128"),
1094 "the error must name the type and the limit, got: {message}"
1095 );
1096 }
1097
1098 #[test]
1099 fn space_nesting_at_the_serialize_limit_is_accepted_and_one_deeper_is_not() {
1100 let (accepted, rejected) = on_stack(PRODUCTION_STACK, || {
1104 let at_limit = analyze_nested(MAX_SPACE_SERIALIZE_DEPTH);
1105 let past_limit = analyze_nested(MAX_SPACE_SERIALIZE_DEPTH + 1);
1106 (
1107 [
1108 serde_json::to_string(&at_limit).is_ok(),
1109 serde_yaml::to_string(&at_limit).is_ok(),
1110 toml::to_string(&at_limit).is_ok(),
1111 {
1112 let mut bytes = Vec::new();
1113 ciborium::into_writer(&at_limit, &mut bytes).is_ok()
1114 },
1115 ],
1116 [
1117 serde_json::to_string(&past_limit).is_ok(),
1118 serde_yaml::to_string(&past_limit).is_ok(),
1119 toml::to_string(&past_limit).is_ok(),
1120 {
1121 let mut bytes = Vec::new();
1122 ciborium::into_writer(&past_limit, &mut bytes).is_ok()
1123 },
1124 ],
1125 )
1126 });
1127 assert_eq!(
1128 accepted, [true; 4],
1129 "exactly {MAX_SPACE_SERIALIZE_DEPTH} levels must serialize in every format"
1130 );
1131 assert_eq!(
1132 rejected, [false; 4],
1133 "one level past the limit must be refused in every format"
1134 );
1135 }
1136
1137 #[test]
1138 fn deeply_nested_ops_convert_and_serialize_without_stack_overflow() {
1139 const DEPTH: usize = 2_000;
1142 let (converted_depth, message) = on_stack(PRODUCTION_STACK, || {
1143 let ops = crate::Ast::parse(crate::Source::new(
1144 crate::LANG::Rust,
1145 nested_functions(DEPTH).as_bytes(),
1146 ))
1147 .expect("nested-function fixture must parse")
1148 .ops()
1149 .expect("nested-function fixture must yield ops");
1150 let wire = Ops::from(&ops);
1151 let mut deepest = 0;
1152 let mut stack = vec![(&wire, 1_usize)];
1153 while let Some((node, depth)) = stack.pop() {
1154 deepest = deepest.max(depth);
1155 for child in &node.spaces {
1156 stack.push((child, depth + 1));
1157 }
1158 }
1159 let message = serde_json::to_string(&ops)
1160 .expect_err("nesting past the limit must fail, not serialize")
1161 .to_string();
1162 (deepest, message)
1163 });
1164 assert_eq!(converted_depth, DEPTH + 1, "the whole chain must convert");
1165 assert!(
1166 message.contains("Ops nesting is deeper than the serialization limit of 128"),
1167 "the error must name the type and the limit, got: {message}"
1168 );
1169 }
1170}