1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
12#[serde(rename_all = "snake_case")]
13pub enum IndentStyle {
14 Tabs,
15 Spaces2,
16 Spaces4,
17 Spaces8,
18 Mixed,
19 #[default]
20 Unknown,
21}
22
23impl IndentStyle {
24 #[must_use]
25 pub const fn display(self) -> &'static str {
26 match self {
27 Self::Tabs => "Tabs",
28 Self::Spaces2 => "2-Space",
29 Self::Spaces4 => "4-Space",
30 Self::Spaces8 => "8-Space",
31 Self::Mixed => "Mixed",
32 Self::Unknown => "\u{2014}",
33 }
34 }
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct StyleSignal {
42 pub name: String,
44 pub value: String,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct StyleGuideScore {
51 pub name: String,
52 pub description: String,
54 pub score_pct: u8,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, Default)]
60pub struct StyleAnalysis {
61 pub language_family: String,
63
64 pub indent_style: IndentStyle,
66 pub tab_indented_lines: u32,
67 pub space2_indented_lines: u32,
68 pub space4_indented_lines: u32,
69 pub lines_over_80: u32,
70 pub lines_over_100: u32,
71 pub lines_over_120: u32,
72 pub max_line_length: u32,
73 pub total_lines: u32,
74
75 pub signals: Vec<StyleSignal>,
77
78 pub guide_scores: Vec<StyleGuideScore>,
80 pub dominant_guide: String,
81 pub dominant_score_pct: u8,
82}
83
84pub fn scan_indent(line: &str, tabs: &mut u32, sp2: &mut u32, sp4: &mut u32) {
88 let Some(first) = line.chars().next() else {
89 return;
90 };
91 if first == '\t' {
92 *tabs += 1;
93 return;
94 }
95 if first != ' ' {
96 return;
97 }
98 let leading = line.bytes().take_while(|&b| b == b' ').count();
99 if leading == 0 {
100 return;
101 }
102 if leading % 4 == 0 {
103 *sp4 += 1;
104 } else if leading % 2 == 0 {
105 *sp2 += 1;
106 }
107}
108
109#[must_use]
111pub fn classify_indent(tabs: u32, sp2: u32, sp4: u32) -> IndentStyle {
112 let total = tabs + sp2 + sp4;
113 if total == 0 {
114 return IndentStyle::Unknown;
115 }
116 let tab_pct = f64::from(tabs) / f64::from(total);
117 let s2_pct = f64::from(sp2) / f64::from(total);
118 let s4_pct = f64::from(sp4) / f64::from(total);
119 if tab_pct >= 0.60 {
120 return IndentStyle::Tabs;
121 }
122 if s4_pct >= 0.60 {
123 return IndentStyle::Spaces4;
124 }
125 if s2_pct >= 0.60 {
126 return IndentStyle::Spaces2;
127 }
128 if sp4 > sp2 * 2 && sp4 > tabs {
129 return IndentStyle::Spaces4;
130 }
131 if sp2 > sp4 && sp2 > tabs {
132 return IndentStyle::Spaces2;
133 }
134 IndentStyle::Mixed
135}
136
137#[must_use]
141#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] pub fn weighted_score(features: &[(f32, f32)]) -> u8 {
143 let s: f32 = features.iter().map(|(w, v)| w * v).sum();
144 (s * 100.0).round().clamp(0.0, 100.0) as u8
145}
146
147#[must_use]
148pub const fn score_indent_2(s: IndentStyle) -> f32 {
149 match s {
150 IndentStyle::Spaces2 => 1.0,
151 IndentStyle::Mixed => 0.35,
152 _ => 0.05,
153 }
154}
155
156#[must_use]
157pub const fn score_indent_4(s: IndentStyle) -> f32 {
158 match s {
159 IndentStyle::Spaces4 => 1.0,
160 IndentStyle::Mixed => 0.35,
161 _ => 0.05,
162 }
163}
164
165#[must_use]
166pub const fn score_indent_tabs(s: IndentStyle) -> f32 {
167 match s {
168 IndentStyle::Tabs => 1.0,
169 IndentStyle::Mixed => 0.20,
170 _ => 0.05,
171 }
172}
173
174#[must_use]
176pub fn score_line80(over: u32, total: u32) -> f32 {
177 if total == 0 {
178 return 1.0;
179 }
180 let p = f64::from(over) / f64::from(total);
181 if p < 0.02 {
182 1.00
183 } else if p < 0.08 {
184 0.75
185 } else if p < 0.20 {
186 0.45
187 } else {
188 0.10
189 }
190}
191
192#[must_use]
194pub fn score_line88(over88: u32, total: u32) -> f32 {
195 score_line_n(over88, total)
196}
197
198#[must_use]
200pub fn score_line100(over100: u32, total: u32) -> f32 {
201 score_line_n(over100, total)
202}
203
204#[must_use]
206pub fn score_line120(over120: u32, total: u32) -> f32 {
207 score_line_n(over120, total)
208}
209
210#[must_use]
211pub fn score_line_n(over: u32, total: u32) -> f32 {
212 if total == 0 {
213 return 1.0;
214 }
215 let p = f64::from(over) / f64::from(total);
216 if p < 0.03 {
217 1.00
218 } else if p < 0.10 {
219 0.75
220 } else if p < 0.25 {
221 0.45
222 } else {
223 0.10
224 }
225}
226
227#[must_use]
229pub fn count_over(lines: &[&str], limit: usize) -> u32 {
230 u32::try_from(lines.iter().filter(|l| l.len() > limit).count()).unwrap_or(u32::MAX)
231}
232
233#[must_use]
237pub fn top_guide(scores: &[StyleGuideScore]) -> (String, u8) {
238 scores
239 .iter()
240 .max_by_key(|s| s.score_pct)
241 .map_or_else(|| ("Unknown".into(), 0), |s| (s.name.clone(), s.score_pct))
242}
243
244pub struct BaseMetrics {
248 pub tabs: u32,
249 pub sp2: u32,
250 pub sp4: u32,
251 pub over80: u32,
252 pub over100: u32,
253 pub over120: u32,
254 pub max_len: u32,
255 pub total: u32,
256}
257
258#[must_use]
260pub fn scan_base_metrics(lines: &[&str]) -> BaseMetrics {
261 let over80 = count_over(lines, 80);
262 let over100 = count_over(lines, 100);
263 let over120 = count_over(lines, 120);
264 let max_len = lines
265 .iter()
266 .map(|l| u32::try_from(l.len()).unwrap_or(u32::MAX))
267 .max()
268 .unwrap_or(0);
269 let total = u32::try_from(lines.len()).unwrap_or(u32::MAX);
270 let mut tabs = 0u32;
271 let mut sp2 = 0u32;
272 let mut sp4 = 0u32;
273 for line in lines {
274 scan_indent(line, &mut tabs, &mut sp2, &mut sp4);
275 }
276 BaseMetrics {
277 tabs,
278 sp2,
279 sp4,
280 over80,
281 over100,
282 over120,
283 max_len,
284 total,
285 }
286}
287
288pub fn count_first_quote(trimmed: &str, single_q: &mut u32, double_q: &mut u32) {
291 for ch in trimmed.chars() {
292 if ch == '\'' {
293 *single_q += 1;
294 break;
295 }
296 if ch == '"' {
297 *double_q += 1;
298 break;
299 }
300 }
301}
302
303#[derive(Clone, Copy, PartialEq, Eq)]
307pub enum BraceStyle {
308 Attach,
309 Allman,
310 Mixed,
311 Unknown,
312}
313
314impl BraceStyle {
315 #[must_use]
316 pub const fn display(self) -> &'static str {
317 match self {
318 Self::Attach => "K&R / Attach",
319 Self::Allman => "Allman",
320 Self::Mixed => "Mixed",
321 Self::Unknown => "\u{2014}",
322 }
323 }
324}
325
326#[must_use]
328pub fn classify_brace(allman: u32, attach: u32) -> BraceStyle {
329 let t = allman + attach;
330 if t == 0 {
331 return BraceStyle::Unknown;
332 }
333 let a = f64::from(allman) / f64::from(t);
334 let k = f64::from(attach) / f64::from(t);
335 if a >= 0.65 {
336 BraceStyle::Allman
337 } else if k >= 0.65 {
338 BraceStyle::Attach
339 } else {
340 BraceStyle::Mixed
341 }
342}
343
344#[must_use]
346pub const fn score_attach_brace(b: BraceStyle) -> f32 {
347 match b {
348 BraceStyle::Attach => 1.0,
349 BraceStyle::Mixed => 0.40,
350 BraceStyle::Allman => 0.05,
351 BraceStyle::Unknown => 0.50,
352 }
353}
354
355#[must_use]
357pub const fn score_allman_brace(b: BraceStyle) -> f32 {
358 match b {
359 BraceStyle::Allman => 1.0,
360 BraceStyle::Mixed => 0.40,
361 BraceStyle::Attach => 0.05,
362 BraceStyle::Unknown => 0.50,
363 }
364}
365
366impl StyleAnalysis {
367 #[must_use]
370 pub fn assemble(
371 language_family: &str,
372 indent: IndentStyle,
373 m: &BaseMetrics,
374 signals: Vec<StyleSignal>,
375 guides: Vec<StyleGuideScore>,
376 ) -> Self {
377 let (dominant, dominant_pct) = top_guide(&guides);
378 Self {
379 language_family: language_family.into(),
380 indent_style: indent,
381 tab_indented_lines: m.tabs,
382 space2_indented_lines: m.sp2,
383 space4_indented_lines: m.sp4,
384 lines_over_80: m.over80,
385 lines_over_100: m.over100,
386 lines_over_120: m.over120,
387 max_line_length: m.max_len,
388 total_lines: m.total,
389 signals,
390 guide_scores: guides,
391 dominant_guide: dominant,
392 dominant_score_pct: dominant_pct,
393 }
394 }
395}
396
397#[cfg(test)]
400mod tests {
401 use super::*;
402
403 #[test]
406 fn indent_style_display_all_variants() {
407 assert_eq!(IndentStyle::Tabs.display(), "Tabs");
408 assert_eq!(IndentStyle::Spaces2.display(), "2-Space");
409 assert_eq!(IndentStyle::Spaces4.display(), "4-Space");
410 assert_eq!(IndentStyle::Spaces8.display(), "8-Space");
411 assert_eq!(IndentStyle::Mixed.display(), "Mixed");
412 assert!(!IndentStyle::Unknown.display().is_empty());
414 }
415
416 #[test]
419 fn scan_indent_tab_line() {
420 let mut tabs = 0u32;
421 let mut sp2 = 0u32;
422 let mut sp4 = 0u32;
423 scan_indent("\treturn x;", &mut tabs, &mut sp2, &mut sp4);
424 assert_eq!(tabs, 1);
425 assert_eq!(sp2, 0);
426 assert_eq!(sp4, 0);
427 }
428
429 #[test]
430 fn scan_indent_two_space_line() {
431 let mut tabs = 0u32;
432 let mut sp2 = 0u32;
433 let mut sp4 = 0u32;
434 scan_indent(" let x = 1;", &mut tabs, &mut sp2, &mut sp4);
435 assert_eq!(tabs, 0);
436 assert_eq!(sp2, 1);
437 assert_eq!(sp4, 0);
438 }
439
440 #[test]
441 fn scan_indent_four_space_line() {
442 let mut tabs = 0u32;
443 let mut sp2 = 0u32;
444 let mut sp4 = 0u32;
445 scan_indent(" let x = 1;", &mut tabs, &mut sp2, &mut sp4);
446 assert_eq!(tabs, 0);
447 assert_eq!(sp2, 0);
448 assert_eq!(sp4, 1);
449 }
450
451 #[test]
452 fn scan_indent_eight_space_line() {
453 let mut tabs = 0u32;
454 let mut sp2 = 0u32;
455 let mut sp4 = 0u32;
456 scan_indent(" let x = 1;", &mut tabs, &mut sp2, &mut sp4);
457 assert_eq!(sp4, 1);
459 }
460
461 #[test]
462 fn scan_indent_empty_line_no_change() {
463 let mut tabs = 0u32;
464 let mut sp2 = 0u32;
465 let mut sp4 = 0u32;
466 scan_indent("", &mut tabs, &mut sp2, &mut sp4);
467 assert_eq!(tabs, 0);
468 assert_eq!(sp2, 0);
469 assert_eq!(sp4, 0);
470 }
471
472 #[test]
473 fn scan_indent_no_leading_whitespace() {
474 let mut tabs = 0u32;
475 let mut sp2 = 0u32;
476 let mut sp4 = 0u32;
477 scan_indent("let x = 1;", &mut tabs, &mut sp2, &mut sp4);
478 assert_eq!(tabs, 0);
479 assert_eq!(sp2, 0);
480 assert_eq!(sp4, 0);
481 }
482
483 #[test]
484 fn scan_indent_three_spaces_not_counted() {
485 let mut tabs = 0u32;
487 let mut sp2 = 0u32;
488 let mut sp4 = 0u32;
489 scan_indent(" let x = 1;", &mut tabs, &mut sp2, &mut sp4);
490 assert_eq!(tabs, 0);
491 assert_eq!(sp2, 0);
492 assert_eq!(sp4, 0);
493 }
494
495 #[test]
498 fn classify_indent_all_zero_returns_unknown() {
499 assert_eq!(classify_indent(0, 0, 0), IndentStyle::Unknown);
500 }
501
502 #[test]
503 fn classify_indent_dominant_tabs() {
504 assert_eq!(classify_indent(10, 1, 1), IndentStyle::Tabs);
506 }
507
508 #[test]
509 fn classify_indent_dominant_spaces4() {
510 assert_eq!(classify_indent(0, 0, 10), IndentStyle::Spaces4);
511 }
512
513 #[test]
514 fn classify_indent_dominant_spaces2() {
515 assert_eq!(classify_indent(0, 10, 0), IndentStyle::Spaces2);
516 }
517
518 #[test]
519 fn classify_indent_mixed_when_evenly_split() {
520 assert_eq!(classify_indent(3, 3, 3), IndentStyle::Mixed);
522 }
523
524 #[test]
525 fn classify_indent_spaces4_when_sp4_greatly_exceeds_sp2() {
526 assert_eq!(classify_indent(0, 1, 10), IndentStyle::Spaces4);
528 }
529
530 #[test]
531 fn classify_indent_spaces2_when_sp2_exceeds_others() {
532 assert_eq!(classify_indent(0, 5, 2), IndentStyle::Spaces2);
534 }
535
536 #[test]
539 fn score_indent_2_perfect() {
540 assert!((super::score_indent_2(IndentStyle::Spaces2) - 1.0).abs() < f32::EPSILON);
541 }
542
543 #[test]
544 fn score_indent_2_mixed() {
545 assert!((super::score_indent_2(IndentStyle::Mixed) - 0.35).abs() < 0.01);
546 }
547
548 #[test]
549 fn score_indent_4_perfect() {
550 assert!((super::score_indent_4(IndentStyle::Spaces4) - 1.0).abs() < f32::EPSILON);
551 }
552
553 #[test]
554 fn score_indent_tabs_perfect() {
555 assert!((super::score_indent_tabs(IndentStyle::Tabs) - 1.0).abs() < f32::EPSILON);
556 }
557
558 #[test]
559 fn score_indent_tabs_mixed() {
560 assert!((super::score_indent_tabs(IndentStyle::Mixed) - 0.20).abs() < 0.01);
561 }
562
563 #[test]
566 fn score_line80_zero_total_is_perfect() {
567 assert!((score_line80(0, 0) - 1.0).abs() < f32::EPSILON);
568 }
569
570 #[test]
571 fn score_line80_very_few_violations() {
572 let score = score_line80(1, 100);
574 assert!((score - 1.0).abs() < f32::EPSILON);
575 }
576
577 #[test]
578 fn score_line80_some_violations() {
579 let score = score_line80(5, 100);
581 assert!((score - 0.75).abs() < 0.01);
582 }
583
584 #[test]
585 fn score_line80_many_violations() {
586 let score = score_line80(15, 100);
588 assert!((score - 0.45).abs() < 0.01);
589 }
590
591 #[test]
592 fn score_line80_excessive_violations() {
593 let score = score_line80(30, 100);
595 assert!((score - 0.10).abs() < 0.01);
596 }
597
598 #[test]
601 fn score_line_n_zero_total_is_perfect() {
602 assert!((score_line_n(0, 0) - 1.0).abs() < f32::EPSILON);
603 assert!((score_line100(0, 0) - 1.0).abs() < f32::EPSILON);
604 assert!((score_line120(0, 0) - 1.0).abs() < f32::EPSILON);
605 assert!((score_line88(0, 0) - 1.0).abs() < f32::EPSILON);
606 }
607
608 #[test]
609 fn score_line_n_low_violations_is_perfect() {
610 let score = score_line_n(2, 100);
612 assert!((score - 1.0).abs() < f32::EPSILON);
613 }
614
615 #[test]
616 fn score_line_n_moderate_violations() {
617 let score = score_line_n(7, 100);
619 assert!((score - 0.75).abs() < 0.01);
620 }
621
622 #[test]
623 fn score_line_n_high_violations() {
624 let score = score_line_n(15, 100);
626 assert!((score - 0.45).abs() < 0.01);
627 }
628
629 #[test]
630 fn score_line_n_very_high_violations() {
631 let score = score_line_n(30, 100);
633 assert!((score - 0.10).abs() < 0.01);
634 }
635
636 #[test]
639 fn count_over_counts_lines_exceeding_limit() {
640 let lines = vec![
641 "short",
642 "a longer line that exceeds eighty characters in total when measured carefully",
643 "tiny",
644 ];
645 let count = count_over(&lines, 20);
646 assert_eq!(count, 1);
647 }
648
649 #[test]
650 fn count_over_none_exceeding_returns_zero() {
651 let lines = vec!["hi", "ok", "yes"];
652 assert_eq!(count_over(&lines, 80), 0);
653 }
654
655 #[test]
656 fn count_over_empty_slice_returns_zero() {
657 assert_eq!(count_over(&[], 80), 0);
658 }
659
660 #[test]
663 fn top_guide_returns_highest_scoring_guide() {
664 let guides = vec![
665 StyleGuideScore {
666 name: "A".into(),
667 description: String::new(),
668 score_pct: 70,
669 },
670 StyleGuideScore {
671 name: "B".into(),
672 description: String::new(),
673 score_pct: 95,
674 },
675 StyleGuideScore {
676 name: "C".into(),
677 description: String::new(),
678 score_pct: 80,
679 },
680 ];
681 let (name, score) = top_guide(&guides);
682 assert_eq!(name, "B");
683 assert_eq!(score, 95);
684 }
685
686 #[test]
687 fn top_guide_empty_slice_returns_unknown() {
688 let (name, score) = top_guide(&[]);
689 assert_eq!(name, "Unknown");
690 assert_eq!(score, 0);
691 }
692
693 #[test]
696 fn scan_base_metrics_all_fields() {
697 let lines = vec![
698 " let x = 1;", "\tlet y = 2;", " return z;", "x".repeat(90).leak(), "short",
703 ];
704 let m = scan_base_metrics(&lines);
705 assert_eq!(m.total, 5);
706 assert!(m.tabs >= 1);
707 assert!(m.sp4 >= 1);
708 assert!(m.sp2 >= 1);
709 assert!(m.over80 >= 1);
710 assert_eq!(m.over100, 0);
711 assert_eq!(m.over120, 0);
712 assert!(m.max_len >= 90);
713 }
714
715 #[test]
716 fn scan_base_metrics_empty_input() {
717 let m = scan_base_metrics(&[]);
718 assert_eq!(m.total, 0);
719 assert_eq!(m.max_len, 0);
720 }
721
722 #[test]
725 fn count_first_quote_single_quote_counted() {
726 let mut sq = 0u32;
727 let mut dq = 0u32;
728 count_first_quote("let x = 'hello';", &mut sq, &mut dq);
729 assert_eq!(sq, 1);
730 assert_eq!(dq, 0);
731 }
732
733 #[test]
734 fn count_first_quote_double_quote_counted() {
735 let mut sq = 0u32;
736 let mut dq = 0u32;
737 count_first_quote("let x = \"hello\";", &mut sq, &mut dq);
738 assert_eq!(sq, 0);
739 assert_eq!(dq, 1);
740 }
741
742 #[test]
743 fn count_first_quote_no_quote_no_change() {
744 let mut sq = 0u32;
745 let mut dq = 0u32;
746 count_first_quote("let x = 42;", &mut sq, &mut dq);
747 assert_eq!(sq, 0);
748 assert_eq!(dq, 0);
749 }
750
751 #[test]
752 fn count_first_quote_only_increments_first_found() {
753 let mut sq = 0u32;
755 let mut dq = 0u32;
756 count_first_quote("'a' + \"b\"", &mut sq, &mut dq);
757 assert_eq!(sq, 1);
758 assert_eq!(dq, 0);
759 }
760
761 #[test]
764 fn brace_style_display_all_variants() {
765 assert_eq!(BraceStyle::Attach.display(), "K&R / Attach");
766 assert_eq!(BraceStyle::Allman.display(), "Allman");
767 assert_eq!(BraceStyle::Mixed.display(), "Mixed");
768 assert!(!BraceStyle::Unknown.display().is_empty());
769 }
770
771 #[test]
774 fn classify_brace_unknown_when_both_zero() {
775 assert!(matches!(classify_brace(0, 0), BraceStyle::Unknown));
776 }
777
778 #[test]
779 fn classify_brace_allman_dominant() {
780 assert!(matches!(classify_brace(7, 3), BraceStyle::Allman));
782 }
783
784 #[test]
785 fn classify_brace_attach_dominant() {
786 assert!(matches!(classify_brace(2, 8), BraceStyle::Attach));
787 }
788
789 #[test]
790 fn classify_brace_mixed_when_even() {
791 assert!(matches!(classify_brace(5, 5), BraceStyle::Mixed));
792 }
793
794 #[test]
797 fn score_attach_brace_all_variants() {
798 assert!((score_attach_brace(BraceStyle::Attach) - 1.0).abs() < f32::EPSILON);
799 assert!((score_attach_brace(BraceStyle::Mixed) - 0.40).abs() < 0.01);
800 assert!((score_attach_brace(BraceStyle::Allman) - 0.05).abs() < 0.01);
801 assert!((score_attach_brace(BraceStyle::Unknown) - 0.50).abs() < 0.01);
802 }
803
804 #[test]
805 fn score_allman_brace_all_variants() {
806 assert!((score_allman_brace(BraceStyle::Allman) - 1.0).abs() < f32::EPSILON);
807 assert!((score_allman_brace(BraceStyle::Mixed) - 0.40).abs() < 0.01);
808 assert!((score_allman_brace(BraceStyle::Attach) - 0.05).abs() < 0.01);
809 assert!((score_allman_brace(BraceStyle::Unknown) - 0.50).abs() < 0.01);
810 }
811
812 #[test]
815 fn weighted_score_single_feature_perfect() {
816 let score = weighted_score(&[(1.0, 1.0)]);
817 assert_eq!(score, 100);
818 }
819
820 #[test]
821 fn weighted_score_single_feature_zero() {
822 let score = weighted_score(&[(1.0, 0.0)]);
823 assert_eq!(score, 0);
824 }
825
826 #[test]
827 fn weighted_score_mixed_features() {
828 let score = weighted_score(&[(0.5, 1.0), (0.5, 0.0)]);
830 assert_eq!(score, 50);
831 }
832
833 #[test]
834 fn weighted_score_clamped_to_100() {
835 let score = weighted_score(&[(2.0, 1.0)]);
837 assert_eq!(score, 100);
838 }
839
840 #[test]
841 fn weighted_score_empty_features_is_zero() {
842 assert_eq!(weighted_score(&[]), 0);
843 }
844
845 #[test]
848 fn style_analysis_assemble_sets_dominant_guide() {
849 let m = scan_base_metrics(&[" let x = 1;", "\tlet y = 2;"]);
850 let indent = classify_indent(m.tabs, m.sp2, m.sp4);
851 let guides = vec![
852 StyleGuideScore {
853 name: "Guide A".into(),
854 description: "desc".into(),
855 score_pct: 60,
856 },
857 StyleGuideScore {
858 name: "Guide B".into(),
859 description: "desc".into(),
860 score_pct: 85,
861 },
862 ];
863 let analysis = StyleAnalysis::assemble("TestLang", indent, &m, vec![], guides);
864 assert_eq!(analysis.language_family, "TestLang");
865 assert_eq!(analysis.dominant_guide, "Guide B");
866 assert_eq!(analysis.dominant_score_pct, 85);
867 assert_eq!(analysis.total_lines, 2);
868 }
869}