1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt;
3use std::fmt::Write as _;
4
5use serde::ser::{Error as _, SerializeStruct};
6use serde::{Deserialize, Serialize, Serializer};
7
8use cargo_metadata::Metadata;
9
10use crate::execution::ScanExecution;
11use crate::policy::{CorpusMeasurement, RuleTier, UNMEASURED_NOISE_BASIS_POINTS};
12use crate::report::{Diagnostic, Severity, Status};
13use crate::source_kernel::SourceMeasurement;
14
15mod density;
16mod source_inventory;
17
18use density::{DensityScope, Scale, calculate_dimensions, contributions};
19
20#[derive(Debug, Clone, Copy, Default)]
23pub(crate) struct SourceFileInventory {
24 pub(crate) files: usize,
25 pub(crate) production_lines: usize,
28 pub(crate) complete: bool,
29}
30
31pub(crate) fn source_file_inventory(
32 metadata: &Metadata,
33 scan: Option<&ScanExecution>,
34 measurement: Option<&SourceMeasurement>,
35) -> SourceFileInventory {
36 source_inventory::collect(metadata, scan, measurement)
37}
38
39pub const SCORE_MODEL: &str = "core-v3";
40const SHARE_BASE_URL: &str = "https://rust-doctor.com/share";
41const MAX_SHARED_COUNT: usize = 1_000_000;
42
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct Audit {
45 pub source_files: usize,
46 pub production_lines: usize,
50 pub categories: Vec<AuditCategory>,
51 pub score: Option<AuditScore>,
52 inventory_is_complete: bool,
61}
62
63#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
69pub struct SeverityCounts {
70 pub errors: usize,
71 pub warnings: usize,
72 pub info: usize,
73 pub unknown: usize,
74 pub total: usize,
75}
76
77impl SeverityCounts {
78 pub(crate) fn add(&mut self, severity: Severity, count: usize) {
79 let bucket = match severity {
80 Severity::Error => &mut self.errors,
81 Severity::Warning => &mut self.warnings,
82 Severity::Info => &mut self.info,
83 Severity::Unknown => &mut self.unknown,
84 };
85 *bucket = bucket.saturating_add(count);
86 self.total = self.total.saturating_add(count);
87 }
88
89 fn merge(&mut self, other: Self) {
91 self.errors = self.errors.saturating_add(other.errors);
92 self.warnings = self.warnings.saturating_add(other.warnings);
93 self.info = self.info.saturating_add(other.info);
94 self.unknown = self.unknown.saturating_add(other.unknown);
95 self.total = self.total.saturating_add(other.total);
96 }
97
98 const fn is_coherent(self) -> bool {
99 self.errors
100 .saturating_add(self.warnings)
101 .saturating_add(self.info)
102 .saturating_add(self.unknown)
103 == self.total
104 }
105
106 const fn covers(self, distinct: Self) -> bool {
108 self.errors >= distinct.errors
109 && self.warnings >= distinct.warnings
110 && self.info >= distinct.info
111 && self.unknown >= distinct.unknown
112 && self.total >= distinct.total
113 }
114}
115
116#[derive(Debug, Clone, PartialEq, Eq)]
117pub struct AuditCategory {
118 pub name: AuditCategoryName,
119 pub distinct: SeverityCounts,
120 pub occurrences: SeverityCounts,
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
129pub enum AuditCategoryName {
130 Security,
131 Bugs,
132 Performance,
133 Dependencies,
134 Maintainability,
135 Other,
139}
140
141#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
142pub struct AuditScore {
143 pub model: String,
144 pub value: u8,
145 pub label: ScoreLabel,
146 pub authoritative: bool,
147 pub dimensions: ScoreDimensions,
148 pub worst_tier: Option<RuleTier>,
151 pub applied_ceiling: Option<u8>,
155 pub projected_after_top_three: Option<u8>,
156 pub projected_rule_ids: Vec<String>,
157 #[serde(skip_serializing_if = "Vec::is_empty")]
165 pub withheld_rule_ids: Vec<String>,
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
169pub struct ScoreDimensions {
170 pub security: u8,
171 pub reliability: u8,
172 pub maintainability: u8,
173 pub performance: u8,
174 pub dependencies: u8,
175}
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
178pub enum ScoreLabel {
179 Great,
180 #[serde(rename = "Needs work")]
181 NeedsWork,
182 Critical,
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq)]
186pub enum ShareError {
187 ScoreUnavailable,
188 NonAuthoritative,
189 InvalidPayload,
190}
191
192impl fmt::Display for ShareError {
193 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
194 formatter.write_str(match self {
195 Self::ScoreUnavailable => "the audit does not contain a score",
196 Self::NonAuthoritative => "the audit score is not authoritative",
197 Self::InvalidPayload => "the audit exceeds the public share bounds",
198 })
199 }
200}
201
202impl std::error::Error for ShareError {}
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
205pub(crate) enum ScoreDimension {
206 Security,
207 Reliability,
208 Maintainability,
209 Performance,
210 Dependencies,
211}
212
213impl ScoreDimension {
214 const ALL: [Self; 5] = [
219 Self::Security,
220 Self::Reliability,
221 Self::Maintainability,
222 Self::Performance,
223 Self::Dependencies,
224 ];
225}
226
227#[derive(Debug, Clone, PartialEq, Eq)]
228pub(crate) struct RuleAggregate {
229 pub(crate) id: String,
230 pub(crate) effective_severity: Severity,
231 pub(crate) category: Option<AuditCategoryName>,
232 dimension: Option<ScoreDimension>,
233 scope: DensityScope,
235 tier: Option<RuleTier>,
236 pub(crate) occurrences: usize,
238 numerator: u64,
243 noise: Option<u16>,
248 contribution: u64,
255}
256
257#[derive(Debug)]
258pub(crate) struct RuleAggregation {
259 pub(crate) rules: Vec<RuleAggregate>,
260 scale: Scale,
261 diagnostics_are_authoritative: bool,
262}
263
264#[derive(Debug)]
265struct PendingRule {
266 id: String,
267 severity: Severity,
268 category: Option<AuditCategoryName>,
269 dimension: Option<ScoreDimension>,
270 scope: DensityScope,
271 mapping_conflict: bool,
272 tier: Option<RuleTier>,
273 occurrences: usize,
274 numerator: u64,
275 noise: Option<u16>,
276}
277
278impl Audit {
279 pub fn build(
280 source_files: usize,
281 production_lines: usize,
282 status: Status,
283 diagnostics: &[Diagnostic],
284 ) -> Self {
285 Self::build_with_inventory(
286 SourceFileInventory {
287 files: source_files,
288 production_lines,
289 complete: true,
290 },
291 status,
292 diagnostics,
293 )
294 }
295
296 pub(crate) fn build_from_inventory(
297 inventory: SourceFileInventory,
298 status: Status,
299 diagnostics: &[Diagnostic],
300 ) -> Self {
301 Self::build_with_inventory(inventory, status, diagnostics)
302 }
303
304 fn build_with_inventory(
305 inventory: SourceFileInventory,
306 status: Status,
307 diagnostics: &[Diagnostic],
308 ) -> Self {
309 let SourceFileInventory {
310 files: source_files,
311 production_lines,
312 complete: inventory_is_complete,
313 } = inventory;
314 let categories = category_tallies(diagnostics);
318 let aggregation = aggregate_rules(production_lines, diagnostics.iter());
319 let scan_is_complete = status == Status::Complete && inventory_is_complete;
320 let score = is_scorable_workspace(source_files, production_lines)
323 .then(|| score(&aggregation, scan_is_complete));
324 Self {
325 source_files,
326 production_lines,
327 categories,
328 score,
329 inventory_is_complete,
330 }
331 }
332
333 pub(crate) fn rebuild_for_scope(&self, status: Status, diagnostics: &[Diagnostic]) -> Self {
334 Self::build_with_inventory(
335 SourceFileInventory {
336 files: self.source_files,
337 production_lines: self.production_lines,
338 complete: self.inventory_is_complete,
339 },
340 status,
341 diagnostics,
342 )
343 }
344
345 pub fn share_url(&self) -> Result<String, ShareError> {
346 let score = self.score.as_ref().ok_or(ShareError::ScoreUnavailable)?;
347 if !score.authoritative {
348 return Err(ShareError::NonAuthoritative);
349 }
350 if !self.is_valid() {
351 return Err(ShareError::InvalidPayload);
352 }
353
354 let (_, occurrences) = self.totals();
357 build_share_url(
358 score.value,
359 occurrences.errors,
360 occurrences.warnings,
361 occurrences.info,
362 self.source_files,
363 self.production_lines,
364 )
365 }
366
367 pub fn is_valid(&self) -> bool {
368 let mut previous = None;
369 let categories_are_valid = self.categories.iter().all(|category| {
370 let ordered = previous.is_none_or(|previous| previous < category.name);
371 previous = Some(category.name);
372 ordered && category.is_valid()
373 });
374 categories_are_valid
375 && is_scorable_workspace(self.source_files, self.production_lines)
376 == self.score.is_some()
377 && self.score.as_ref().is_none_or(AuditScore::is_valid)
378 }
379
380 pub fn totals(&self) -> (SeverityCounts, SeverityCounts) {
382 self.categories.iter().fold(
383 (SeverityCounts::default(), SeverityCounts::default()),
384 |(mut distinct, mut occurrences), category| {
385 distinct.merge(category.distinct);
386 occurrences.merge(category.occurrences);
387 (distinct, occurrences)
388 },
389 )
390 }
391}
392
393impl Serialize for Audit {
394 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
395 where
396 S: Serializer,
397 {
398 if !self.is_valid() {
399 return Err(S::Error::custom("invalid audit state"));
400 }
401 let mut state = serializer.serialize_struct("Audit", 4)?;
402 state.serialize_field("source_files", &self.source_files)?;
403 state.serialize_field("production_lines", &self.production_lines)?;
404 state.serialize_field("categories", &self.categories)?;
405 state.serialize_field("score", &self.score)?;
406 state.end()
407 }
408}
409
410impl AuditCategory {
411 fn empty(name: AuditCategoryName) -> Self {
412 Self {
413 name,
414 distinct: SeverityCounts::default(),
415 occurrences: SeverityCounts::default(),
416 }
417 }
418
419 fn is_valid(&self) -> bool {
420 self.distinct.total > 0
421 && self.distinct.is_coherent()
422 && self.occurrences.is_coherent()
423 && self.occurrences.covers(self.distinct)
424 }
425}
426
427impl Serialize for AuditCategory {
428 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
435 where
436 S: Serializer,
437 {
438 let mut state = serializer.serialize_struct("AuditCategory", 7)?;
439 state.serialize_field("name", &self.name)?;
440 state.serialize_field("errors", &self.occurrences.errors)?;
441 state.serialize_field("warnings", &self.occurrences.warnings)?;
442 state.serialize_field("info", &self.occurrences.info)?;
443 state.serialize_field("unknown", &self.occurrences.unknown)?;
444 state.serialize_field("distinct", &self.distinct)?;
445 state.serialize_field("occurrences", &self.occurrences)?;
446 state.end()
447 }
448}
449
450impl AuditScore {
451 pub fn is_valid(&self) -> bool {
457 if self.model != SCORE_MODEL || self.value > 100 {
458 return false;
459 }
460 if self.applied_ceiling != self.worst_tier.and_then(tier_overall_ceiling) {
461 return false;
462 }
463 if self.value != capped(weighted_score(self.dimensions), self.applied_ceiling) {
464 return false;
465 }
466 if self.label != score_label(self.value) {
467 return false;
468 }
469 if self.dimensions.values().into_iter().any(|value| value > 100) {
470 return false;
471 }
472 if self
474 .projected_after_top_three
475 .is_some_and(|projected| projected > 100 || projected < self.value)
476 {
477 return false;
478 }
479 if !self.projected_rules_are_named_once() {
480 return false;
481 }
482 if self.authoritative {
485 matches!(
486 (
487 self.projected_after_top_three,
488 self.projected_rule_ids.is_empty()
489 ),
490 (None, true) | (Some(_), false)
491 )
492 } else {
493 self.projected_after_top_three.is_none() && self.projected_rule_ids.is_empty()
494 }
495 }
496
497 fn projected_rules_are_named_once(&self) -> bool {
499 let distinct: BTreeSet<_> = self.projected_rule_ids.iter().collect();
500 self.projected_rule_ids.len() <= 3
501 && distinct.len() == self.projected_rule_ids.len()
502 && self.projected_rule_ids.iter().all(|rule| !rule.is_empty())
503 }
504}
505
506impl ScoreDimensions {
507 fn from_fn(mut score_for: impl FnMut(ScoreDimension) -> u8) -> Self {
512 Self {
513 security: score_for(ScoreDimension::Security),
514 reliability: score_for(ScoreDimension::Reliability),
515 maintainability: score_for(ScoreDimension::Maintainability),
516 performance: score_for(ScoreDimension::Performance),
517 dependencies: score_for(ScoreDimension::Dependencies),
518 }
519 }
520
521 fn values(self) -> [u8; 5] {
522 ScoreDimension::ALL.map(|dimension| self.value_for(dimension))
523 }
524
525 fn value_for(self, dimension: ScoreDimension) -> u8 {
526 match dimension {
527 ScoreDimension::Security => self.security,
528 ScoreDimension::Reliability => self.reliability,
529 ScoreDimension::Maintainability => self.maintainability,
530 ScoreDimension::Performance => self.performance,
531 ScoreDimension::Dependencies => self.dependencies,
532 }
533 }
534}
535
536impl ScoreLabel {
537 pub const fn as_str(self) -> &'static str {
538 match self {
539 Self::Great => "Great",
540 Self::NeedsWork => "Needs work",
541 Self::Critical => "Critical",
542 }
543 }
544}
545
546impl fmt::Display for ScoreLabel {
547 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
548 formatter.write_str(self.as_str())
549 }
550}
551
552impl AuditCategoryName {
553 pub const fn as_str(self) -> &'static str {
554 match self {
555 Self::Security => "Security",
556 Self::Bugs => "Bugs",
557 Self::Performance => "Performance",
558 Self::Dependencies => "Dependencies",
559 Self::Maintainability => "Maintainability",
560 Self::Other => "Other",
561 }
562 }
563}
564
565impl fmt::Display for AuditCategoryName {
566 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
567 formatter.write_str(self.as_str())
568 }
569}
570
571pub(crate) fn category_mapping(category: &str) -> Option<(AuditCategoryName, ScoreDimension)> {
572 match category {
573 "security" => Some((AuditCategoryName::Security, ScoreDimension::Security)),
574 "correctness" | "reliability" => {
575 Some((AuditCategoryName::Bugs, ScoreDimension::Reliability))
576 }
577 "performance" => Some((AuditCategoryName::Performance, ScoreDimension::Performance)),
578 "cargo" | "dependencies" => Some((
579 AuditCategoryName::Dependencies,
580 ScoreDimension::Dependencies,
581 )),
582 "maintainability" => Some((
583 AuditCategoryName::Maintainability,
584 ScoreDimension::Maintainability,
585 )),
586 _ => None,
587 }
588}
589
590fn category_bucket(category: Option<&str>) -> AuditCategoryName {
594 category
595 .and_then(category_mapping)
596 .map_or(AuditCategoryName::Other, |(name, _)| name)
597}
598
599const fn tier_dimension_ceiling(tier: RuleTier) -> Option<u8> {
601 match tier {
602 RuleTier::P0 => Some(20),
603 RuleTier::P1 => Some(50),
604 RuleTier::P2 => Some(75),
605 RuleTier::P3 => None,
606 }
607}
608
609const fn tier_overall_ceiling(tier: RuleTier) -> Option<u8> {
611 match tier {
612 RuleTier::P0 => Some(40),
613 RuleTier::P1 => Some(65),
614 RuleTier::P2 | RuleTier::P3 => None,
615 }
616}
617
618const BASIS_POINTS: u64 = 10_000;
621
622const fn is_scorable_workspace(source_files: usize, production_lines: usize) -> bool {
628 source_files > 0 && production_lines > 0
629}
630
631const fn capped(value: u8, ceiling: Option<u8>) -> u8 {
632 match ceiling {
633 Some(ceiling) if ceiling < value => ceiling,
634 _ => value,
635 }
636}
637
638fn worse_tier(current: Option<RuleTier>, candidate: Option<RuleTier>) -> Option<RuleTier> {
640 match (current, candidate) {
641 (Some(current), Some(candidate)) => Some(current.min(candidate)),
642 (current, candidate) => current.or(candidate),
643 }
644}
645
646const fn dimension_weight_twice(dimension: ScoreDimension) -> u64 {
647 match dimension {
648 ScoreDimension::Security => 4,
649 ScoreDimension::Reliability => 3,
650 ScoreDimension::Maintainability
651 | ScoreDimension::Performance
652 | ScoreDimension::Dependencies => 2,
653 }
654}
655
656impl RuleAggregate {
657 pub(crate) const fn contribution(&self) -> u64 {
666 self.contribution
667 }
668
669 pub(crate) fn expected_repair_value(&self) -> u64 {
689 let noise = self.noise.unwrap_or(UNMEASURED_NOISE_BASIS_POINTS);
690 let kept = BASIS_POINTS.saturating_sub(noise as u64);
691 self.contribution().saturating_mul(kept) / BASIS_POINTS
692 }
693
694 const fn scoring_tier(&self) -> Option<RuleTier> {
697 if self.is_scorable() { self.tier } else { None }
698 }
699
700 const fn is_scorable(&self) -> bool {
701 self.numerator > 0 && self.dimension.is_some()
702 }
703}
704
705fn category_tallies(diagnostics: &[Diagnostic]) -> Vec<AuditCategory> {
711 let mut tallies = BTreeMap::<AuditCategoryName, AuditCategory>::new();
712 for diagnostic in diagnostics {
713 let name = category_bucket(diagnostic.category.as_deref());
714 let tally = tallies
715 .entry(name)
716 .or_insert_with(|| AuditCategory::empty(name));
717 tally.distinct.add(diagnostic.severity, 1);
718 tally
719 .occurrences
720 .add(diagnostic.severity, diagnostic.occurrences);
721 }
722
723 tallies.into_values().collect()
724}
725
726fn score(aggregation: &RuleAggregation, scan_complete: bool) -> AuditScore {
727 let scored = ScoredState::of(&aggregation.rules, &BTreeSet::new(), aggregation.scale);
728 let authoritative = scan_complete && aggregation.diagnostics_are_authoritative;
729 let (projected, withheld) = rank_repairs(&aggregation.rules);
730 let projected_rule_ids: Vec<String> = projected
731 .iter()
732 .take(3)
733 .map(|rule| rule.id.clone())
734 .collect();
735
736 let (projected_rule_ids, withheld_rule_ids, projected_after_top_three) = if authoritative {
740 let after = (!projected_rule_ids.is_empty()).then(|| {
741 let removed: BTreeSet<_> = projected_rule_ids.iter().cloned().collect();
742 ScoredState::of(&aggregation.rules, &removed, aggregation.scale).value
743 });
744 let withheld_rule_ids = withheld.iter().map(|rule| rule.id.clone()).collect();
745 (projected_rule_ids, withheld_rule_ids, after)
746 } else {
747 (Vec::new(), Vec::new(), None)
748 };
749
750 AuditScore {
751 model: SCORE_MODEL.to_owned(),
752 value: scored.value,
753 label: score_label(scored.value),
754 authoritative,
755 dimensions: scored.dimensions,
756 worst_tier: scored.worst_tier,
757 applied_ceiling: scored.applied_ceiling,
758 projected_after_top_three,
759 projected_rule_ids,
760 withheld_rule_ids,
761 }
762}
763
764fn rank_repairs(rules: &[RuleAggregate]) -> (Vec<&RuleAggregate>, Vec<&RuleAggregate>) {
779 let (mut projected, mut withheld): (Vec<_>, Vec<_>) = rules
780 .iter()
781 .filter(|rule| rule.is_scorable() && rule.contribution() > 0)
782 .partition(|rule| rule.expected_repair_value() > 0);
783 projected.sort_by(|left, right| {
784 right
785 .expected_repair_value()
786 .cmp(&left.expected_repair_value())
787 .then_with(|| right.contribution().cmp(&left.contribution()))
788 .then_with(|| left.id.cmp(&right.id))
789 });
790 withheld.sort_by(|left, right| {
791 right
792 .contribution()
793 .cmp(&left.contribution())
794 .then_with(|| left.id.cmp(&right.id))
795 });
796 (projected, withheld)
797}
798
799struct ScoredState {
801 dimensions: ScoreDimensions,
802 worst_tier: Option<RuleTier>,
803 applied_ceiling: Option<u8>,
804 value: u8,
805}
806
807impl ScoredState {
808 fn of(rules: &[RuleAggregate], removed: &BTreeSet<String>, scale: Scale) -> Self {
809 let dimensions = calculate_dimensions(rules, removed, scale);
810 let worst_tier = rules
811 .iter()
812 .filter(|rule| !removed.contains(&rule.id))
813 .fold(None, |worst, rule| worse_tier(worst, rule.scoring_tier()));
814 let applied_ceiling = worst_tier.and_then(tier_overall_ceiling);
815 Self {
816 dimensions,
817 worst_tier,
818 applied_ceiling,
819 value: capped(weighted_score(dimensions), applied_ceiling),
820 }
821 }
822}
823
824pub(crate) fn aggregate_rules<'a>(
837 production_lines: usize,
838 diagnostics: impl IntoIterator<Item = &'a Diagnostic>,
839) -> RuleAggregation {
840 let mut diagnostics_are_authoritative = true;
841 let mut rules = BTreeMap::<String, PendingRule>::new();
842 for diagnostic in diagnostics {
843 let weighs = crate::report::DiagnosticContext::weighs(diagnostic);
844 let Some(rule_id) = diagnostic.code.as_deref().filter(|code| !code.is_empty()) else {
845 diagnostics_are_authoritative &= !weighs;
846 continue;
847 };
848 let definition = crate::policy::find(rule_id);
849 let mapping = diagnostic.category.as_deref().and_then(category_mapping);
850 let weight = density::severity_weight(diagnostic.severity);
851 let scores = weighs && weight.is_some() && mapping.is_some();
852 if weighs && !scores {
853 diagnostics_are_authoritative = false;
854 }
855 if weighs && definition.is_none() {
860 diagnostics_are_authoritative = false;
861 }
862 let rule = rules.entry(rule_id.to_owned()).or_insert(PendingRule {
863 id: rule_id.to_owned(),
864 severity: diagnostic.severity,
865 category: None,
866 dimension: None,
867 scope: definition.map_or(DensityScope::Workspace, |definition| {
868 DensityScope::of(definition.producer)
869 }),
870 mapping_conflict: false,
871 tier: definition.map(|definition| definition.tier),
872 occurrences: 0,
873 numerator: 0,
874 noise: crate::policy::corpus_measurement(rule_id)
875 .map(CorpusMeasurement::noise_basis_points),
876 });
877 rule.occurrences = rule.occurrences.saturating_add(diagnostic.occurrences);
878 if diagnostic.severity.rank() < rule.severity.rank() {
879 rule.severity = diagnostic.severity;
880 }
881 if scores
882 && let Some((category, dimension)) = mapping
883 && let Some(weight) = weight
884 {
885 rule.numerator = rule.numerator.saturating_add(weight);
888 if rule.dimension.is_some_and(|current| current != dimension) {
889 rule.mapping_conflict = true;
890 diagnostics_are_authoritative = false;
891 } else if rule.dimension.is_none() {
892 rule.category = Some(category);
893 rule.dimension = Some(dimension);
894 }
895 } else if rule.category.is_none()
896 && let Some((category, _)) = mapping
897 {
898 rule.category = Some(category);
899 }
900 }
901 let scale = Scale::of(production_lines);
902 let mut rules: Vec<RuleAggregate> = rules
903 .into_values()
904 .map(|rule| RuleAggregate {
905 id: rule.id,
906 effective_severity: rule.severity,
907 category: (!rule.mapping_conflict).then_some(rule.category).flatten(),
908 dimension: (!rule.mapping_conflict && rule.numerator > 0)
909 .then_some(rule.dimension)
910 .flatten(),
911 scope: rule.scope,
912 tier: rule.tier,
913 occurrences: rule.occurrences,
914 numerator: rule.numerator,
915 noise: rule.noise,
916 contribution: 0,
917 })
918 .collect();
919 let recovered = contributions(&rules, scale);
920 for (rule, contribution) in rules.iter_mut().zip(recovered) {
921 rule.contribution = contribution;
922 }
923 RuleAggregation {
924 rules,
925 scale,
926 diagnostics_are_authoritative,
927 }
928}
929
930fn weighted_score(dimensions: ScoreDimensions) -> u8 {
931 let numerator = ScoreDimension::ALL.into_iter().fold(0u64, |sum, dimension| {
932 sum.saturating_add(
933 u64::from(dimensions.value_for(dimension)) * dimension_weight_twice(dimension),
934 )
935 });
936 u8::try_from((numerator.saturating_add(6) / 13).min(100)).unwrap_or(100)
937}
938
939const fn score_label(score: u8) -> ScoreLabel {
940 if score >= 75 {
941 ScoreLabel::Great
942 } else if score >= 50 {
943 ScoreLabel::NeedsWork
944 } else {
945 ScoreLabel::Critical
946 }
947}
948
949fn build_share_url(
950 score: u8,
951 errors: usize,
952 warnings: usize,
953 info: usize,
954 source_files: usize,
955 production_lines: usize,
956) -> Result<String, ShareError> {
957 if score > 100
958 || [errors, warnings, info, source_files, production_lines]
959 .into_iter()
960 .any(|count| count > MAX_SHARED_COUNT)
961 {
962 return Err(ShareError::InvalidPayload);
963 }
964
965 let mut url = format!("{SHARE_BASE_URL}?s={score}&m={SCORE_MODEL}");
970 for (key, count) in [
971 ("e", errors),
972 ("w", warnings),
973 ("i", info),
974 ("f", source_files),
975 ("l", production_lines),
976 ] {
977 if count > 0 {
978 let _ = write!(url, "&{key}={count}");
979 }
980 }
981 Ok(url)
982}
983
984#[cfg(test)]
985mod tests;