Skip to main content

aver/checker/
mod.rs

1mod coverage;
2mod intent;
3mod law;
4mod verify;
5
6use crate::ast::{Expr, Literal, Pattern, TopLevel, TypeDef, VerifyBlock, VerifyKind};
7
8pub struct VerifyResult {
9    #[allow(dead_code)]
10    pub fn_name: String,
11    pub passed: usize,
12    pub failed: usize,
13    pub skipped: usize,
14    #[allow(dead_code)]
15    pub failures: Vec<(String, String, String)>, // (expr_src, expected, actual)
16}
17
18pub struct ModuleCheckFindings {
19    pub errors: Vec<CheckFinding>,
20    pub warnings: Vec<CheckFinding>,
21}
22
23pub(crate) type FnSigSummary = (Vec<crate::types::Type>, crate::types::Type, Vec<String>);
24pub(crate) type FnSigMap = std::collections::HashMap<String, FnSigSummary>;
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct CheckFinding {
28    pub line: usize,
29    pub module: Option<String>,
30    pub file: Option<String>,
31    pub message: String,
32}
33
34fn module_name_for_items(items: &[TopLevel]) -> Option<String> {
35    items.iter().find_map(|item| {
36        if let TopLevel::Module(m) = item {
37            Some(m.name.clone())
38        } else {
39            None
40        }
41    })
42}
43
44fn dotted_name(expr: &Expr) -> Option<String> {
45    match expr {
46        Expr::Ident(name) => Some(name.clone()),
47        Expr::Attr(base, field) => {
48            let mut prefix = dotted_name(base)?;
49            prefix.push('.');
50            prefix.push_str(field);
51            Some(prefix)
52        }
53        _ => None,
54    }
55}
56
57fn normalize_constructor_tag(path: &str) -> Option<String> {
58    let mut parts = path.split('.').collect::<Vec<_>>();
59    if parts.len() < 2 {
60        return None;
61    }
62    let variant = parts.pop()?;
63    let type_name = parts.pop()?;
64    Some(format!("{}.{}", type_name, variant))
65}
66
67fn constructor_tag_from_pattern(pattern: &Pattern) -> Option<String> {
68    match pattern {
69        Pattern::Constructor(path, _) => normalize_constructor_tag(path),
70        _ => None,
71    }
72}
73
74fn constructor_tag_from_expr(expr: &Expr) -> Option<String> {
75    match expr {
76        Expr::Attr(_, _) => normalize_constructor_tag(&dotted_name(expr)?),
77        Expr::FnCall(callee, _) => normalize_constructor_tag(&dotted_name(callee)?),
78        Expr::Constructor(name, _) => normalize_constructor_tag(name),
79        _ => None,
80    }
81}
82
83fn expr_is_result_err_case(expr: &Expr) -> bool {
84    match expr {
85        Expr::FnCall(callee, _) => dotted_name(callee)
86            .and_then(|path| normalize_constructor_tag(&path))
87            .is_some_and(|tag| tag == "Result.Err"),
88        Expr::Constructor(name, _) => {
89            normalize_constructor_tag(name).is_some_and(|tag| tag == "Result.Err")
90        }
91        _ => false,
92    }
93}
94
95fn expr_is_result_ok_case(expr: &Expr) -> bool {
96    match expr {
97        Expr::FnCall(callee, _) => dotted_name(callee)
98            .and_then(|path| normalize_constructor_tag(&path))
99            .is_some_and(|tag| tag == "Result.Ok"),
100        Expr::Constructor(name, _) => {
101            normalize_constructor_tag(name).is_some_and(|tag| tag == "Result.Ok")
102        }
103        _ => false,
104    }
105}
106
107fn expr_is_option_none_case(expr: &Expr) -> bool {
108    match expr {
109        Expr::Attr(_, _) => dotted_name(expr)
110            .and_then(|path| normalize_constructor_tag(&path))
111            .is_some_and(|tag| tag == "Option.None"),
112        Expr::Constructor(name, None) => {
113            normalize_constructor_tag(name).is_some_and(|tag| tag == "Option.None")
114        }
115        _ => false,
116    }
117}
118
119fn expr_is_option_some_case(expr: &Expr) -> bool {
120    match expr {
121        Expr::FnCall(callee, _) => dotted_name(callee)
122            .and_then(|path| normalize_constructor_tag(&path))
123            .is_some_and(|tag| tag == "Option.Some"),
124        Expr::Constructor(name, _) => {
125            normalize_constructor_tag(name).is_some_and(|tag| tag == "Option.Some")
126        }
127        _ => false,
128    }
129}
130
131fn expr_is_bool_case(expr: &Expr, expected: bool) -> bool {
132    matches!(expr, Expr::Literal(Literal::Bool(value)) if *value == expected)
133}
134
135fn expr_is_empty_list_case(expr: &Expr) -> bool {
136    matches!(expr, Expr::List(items) if items.is_empty())
137}
138
139fn expr_is_non_empty_list_case(expr: &Expr) -> bool {
140    matches!(expr, Expr::List(items) if !items.is_empty())
141}
142
143fn expr_is_empty_string_case(expr: &Expr) -> bool {
144    matches!(expr, Expr::Literal(Literal::Str(value)) if value.is_empty())
145}
146
147fn expr_is_int_literal_case(expr: &Expr, expected: i64) -> bool {
148    matches!(expr, Expr::Literal(Literal::Int(value)) if *value == expected)
149}
150
151fn verify_cases_block_is_well_formed(block: &VerifyBlock) -> bool {
152    matches!(block.kind, VerifyKind::Cases)
153        && !block.cases.is_empty()
154        && block.cases.iter().all(|(left, right)| {
155            verify_case_calls_target(left, &block.fn_name)
156                && !verify_case_calls_target(right, &block.fn_name)
157        })
158}
159
160fn local_sum_type_constructors(items: &[TopLevel], type_name: &str) -> Option<Vec<String>> {
161    items.iter().find_map(|item| match item {
162        TopLevel::TypeDef(TypeDef::Sum { name, variants, .. }) if name == type_name => Some(
163            variants
164                .iter()
165                .map(|variant| format!("{name}.{}", variant.name))
166                .collect(),
167        ),
168        _ => None,
169    })
170}
171
172fn callee_is_target(callee: &Expr, fn_name: &str) -> bool {
173    matches!(callee, Expr::Ident(name) if name == fn_name)
174}
175
176// Re-export from verify submodule
177use verify::collect_target_call_args;
178use verify::verify_case_calls_target;
179
180// Public re-exports so external callers don't break
181pub use coverage::{collect_verify_coverage_warnings, collect_verify_coverage_warnings_in};
182pub use intent::{
183    check_module_intent, check_module_intent_with_sigs, check_module_intent_with_sigs_in,
184    index_decisions,
185};
186pub use law::{collect_verify_law_dependency_warnings, collect_verify_law_dependency_warnings_in};
187pub use verify::{expr_to_str, merge_verify_blocks, run_verify};