linguini_analyzer/
branch_coverage.rs1use crate::{Diagnostic, QuickFix, Replacement};
2use linguini_syntax::Span;
3use std::collections::BTreeSet;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct NamedSpan {
7 pub name: String,
8 pub span: Span,
9}
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct BranchCoverage<'a> {
13 pub subject: &'a str,
14 pub enum_name: &'a str,
15 pub variants: Vec<NamedSpan>,
16 pub branches: Vec<NamedSpan>,
17 pub span: Span,
18}
19
20impl NamedSpan {
21 pub fn new(name: impl Into<String>, span: Span) -> Self {
22 Self {
23 name: name.into(),
24 span,
25 }
26 }
27}
28
29pub fn analyze_branch_coverage(input: BranchCoverage<'_>) -> Vec<Diagnostic> {
30 if input.branches.iter().any(|branch| branch.name == "_") {
31 return Vec::new();
32 }
33
34 let branch_names: BTreeSet<_> = input
35 .branches
36 .iter()
37 .map(|branch| branch.name.as_str())
38 .collect();
39 let insertion = branch_insertion_span(&input.branches, input.span);
40 let mut diagnostics = Vec::new();
41
42 for variant in input.variants {
43 if !branch_names.contains(variant.name.as_str()) {
44 diagnostics.push(
45 Diagnostic::error(
46 format!(
47 "{} for enum `{}` is missing branch `{}`",
48 input.subject, input.enum_name, variant.name
49 ),
50 input.span,
51 )
52 .with_related(variant.span, "enum variant is declared here")
53 .with_quick_fix(QuickFix::replacement(
54 format!("add branch `{}`", variant.name),
55 Replacement {
56 span: insertion,
57 text: format!("\n{} => TODO", variant.name),
58 },
59 )),
60 );
61 }
62 }
63
64 diagnostics
65}
66
67pub fn require_other_branch(subject: &str, branches: &[NamedSpan], span: Span) -> Vec<Diagnostic> {
68 if branches
69 .iter()
70 .any(|branch| matches!(branch.name.as_str(), "other" | "_"))
71 {
72 Vec::new()
73 } else {
74 let insertion = branch_insertion_span(branches, span);
75 vec![Diagnostic::error(
76 format!("{subject} is missing required `other` branch"),
77 span,
78 )
79 .with_quick_fix(QuickFix::replacement(
80 "add `_` branch",
81 Replacement {
82 span: insertion,
83 text: "\n_ => TODO".to_owned(),
84 },
85 ))]
86 }
87}
88
89fn branch_insertion_span(branches: &[NamedSpan], fallback: Span) -> Span {
90 branches
91 .iter()
92 .map(|branch| Span::new(branch.span.end, branch.span.end))
93 .max_by_key(|span| span.start)
94 .unwrap_or(Span::new(fallback.end, fallback.end))
95}