Skip to main content

linguini_analyzer/
branch_coverage.rs

1use 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}