Implements the exhaustiveness algorithm to detect non-exhaustive pattern matches: - Detects missing Bool patterns (true/false) - Detects missing Option patterns (Some/None) - Detects missing Result patterns (Ok/Err) - Recognizes wildcards and variable patterns as catch-alls - Warns about redundant patterns after catch-all patterns - Integrates with the type checker to report errors Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
409 lines
12 KiB
Rust
409 lines
12 KiB
Rust
//! Pattern match exhaustiveness checking
|
|
//!
|
|
//! Implements the "usefulness" algorithm to check if pattern matches
|
|
//! cover all possible cases.
|
|
|
|
use crate::ast::{Literal, LiteralKind, MatchArm, Pattern};
|
|
use crate::types::{Type, TypeDef, TypeEnv, VariantDef};
|
|
use std::collections::HashSet;
|
|
|
|
/// Result of exhaustiveness checking
|
|
#[derive(Debug, Clone)]
|
|
pub struct ExhaustivenessResult {
|
|
pub is_exhaustive: bool,
|
|
pub missing_patterns: Vec<String>,
|
|
pub redundant_arms: Vec<usize>,
|
|
}
|
|
|
|
/// Check if a match expression is exhaustive
|
|
pub fn check_exhaustiveness(
|
|
scrutinee_type: &Type,
|
|
arms: &[MatchArm],
|
|
env: &TypeEnv,
|
|
) -> ExhaustivenessResult {
|
|
let patterns: Vec<&Pattern> = arms.iter().map(|arm| &arm.pattern).collect();
|
|
|
|
// Check for guards - patterns with guards don't guarantee coverage
|
|
let has_guards = arms.iter().any(|arm| arm.guard.is_some());
|
|
|
|
// Get missing patterns
|
|
let missing = find_missing_patterns(scrutinee_type, &patterns, env);
|
|
|
|
// Find redundant arms (patterns that can never match)
|
|
let redundant = find_redundant_arms(&patterns);
|
|
|
|
// If any pattern has a guard, we can't guarantee exhaustiveness
|
|
// unless there's an unconditional wildcard at the end
|
|
let is_exhaustive = if has_guards {
|
|
// Check if last pattern is an unconditional catch-all
|
|
arms.last()
|
|
.map(|arm| arm.guard.is_none() && is_catch_all(&arm.pattern))
|
|
.unwrap_or(false)
|
|
} else {
|
|
missing.is_empty()
|
|
};
|
|
|
|
ExhaustivenessResult {
|
|
is_exhaustive,
|
|
missing_patterns: missing,
|
|
redundant_arms: redundant,
|
|
}
|
|
}
|
|
|
|
/// Check if a pattern is a catch-all (matches anything)
|
|
fn is_catch_all(pattern: &Pattern) -> bool {
|
|
match pattern {
|
|
Pattern::Wildcard(_) => true,
|
|
Pattern::Var(_) => true,
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
/// Find patterns that are missing from coverage
|
|
fn find_missing_patterns(
|
|
scrutinee_type: &Type,
|
|
patterns: &[&Pattern],
|
|
env: &TypeEnv,
|
|
) -> Vec<String> {
|
|
// If any pattern is a catch-all, nothing is missing
|
|
if patterns.iter().any(|p| is_catch_all(p)) {
|
|
return Vec::new();
|
|
}
|
|
|
|
match scrutinee_type {
|
|
Type::Bool => check_bool_exhaustiveness(patterns),
|
|
Type::Option(inner) => check_option_exhaustiveness(patterns, inner, env),
|
|
Type::Named(name) => check_named_type_exhaustiveness(patterns, name, env),
|
|
Type::Tuple(elements) => check_tuple_exhaustiveness(patterns, elements, env),
|
|
// For other types (Int, String, etc.), we can't enumerate all values
|
|
// So we need a wildcard pattern
|
|
Type::Int | Type::Float | Type::String | Type::Char => {
|
|
vec!["_".to_string()]
|
|
}
|
|
// Unit type has exactly one value
|
|
Type::Unit => Vec::new(),
|
|
// For type variables and other complex types, assume exhaustive if there's any pattern
|
|
_ => {
|
|
if patterns.is_empty() {
|
|
vec!["_".to_string()]
|
|
} else {
|
|
Vec::new()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Check Bool exhaustiveness
|
|
fn check_bool_exhaustiveness(patterns: &[&Pattern]) -> Vec<String> {
|
|
let mut has_true = false;
|
|
let mut has_false = false;
|
|
|
|
for pattern in patterns {
|
|
match pattern {
|
|
Pattern::Literal(Literal {
|
|
kind: LiteralKind::Bool(true),
|
|
..
|
|
}) => has_true = true,
|
|
Pattern::Literal(Literal {
|
|
kind: LiteralKind::Bool(false),
|
|
..
|
|
}) => has_false = true,
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
let mut missing = Vec::new();
|
|
if !has_true {
|
|
missing.push("true".to_string());
|
|
}
|
|
if !has_false {
|
|
missing.push("false".to_string());
|
|
}
|
|
missing
|
|
}
|
|
|
|
/// Check Option exhaustiveness
|
|
fn check_option_exhaustiveness(
|
|
patterns: &[&Pattern],
|
|
_inner: &Type,
|
|
_env: &TypeEnv,
|
|
) -> Vec<String> {
|
|
let mut has_none = false;
|
|
let mut has_some = false;
|
|
|
|
for pattern in patterns {
|
|
if let Pattern::Constructor { name, .. } = pattern {
|
|
match name.name.as_str() {
|
|
"None" => has_none = true,
|
|
"Some" => has_some = true,
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut missing = Vec::new();
|
|
if !has_none {
|
|
missing.push("None".to_string());
|
|
}
|
|
if !has_some {
|
|
missing.push("Some(_)".to_string());
|
|
}
|
|
missing
|
|
}
|
|
|
|
/// Check named type (enum) exhaustiveness
|
|
fn check_named_type_exhaustiveness(
|
|
patterns: &[&Pattern],
|
|
type_name: &str,
|
|
env: &TypeEnv,
|
|
) -> Vec<String> {
|
|
// Look up the type definition
|
|
let typedef = match env.types.get(type_name) {
|
|
Some(td) => td,
|
|
None => return Vec::new(), // Unknown type, assume exhaustive
|
|
};
|
|
|
|
// Handle Result specially since it's common
|
|
if type_name == "Result" {
|
|
return check_result_exhaustiveness(patterns);
|
|
}
|
|
|
|
// Get all constructors for enum types
|
|
let constructors: Vec<&VariantDef> = match typedef {
|
|
TypeDef::Enum(variants) => variants.iter().collect(),
|
|
_ => return Vec::new(), // Not an enum, assume exhaustive
|
|
};
|
|
|
|
// Find which constructors are covered
|
|
let mut covered: HashSet<&str> = HashSet::new();
|
|
for pattern in patterns {
|
|
if let Pattern::Constructor { name, .. } = pattern {
|
|
covered.insert(&name.name);
|
|
}
|
|
}
|
|
|
|
// Find missing constructors
|
|
constructors
|
|
.iter()
|
|
.filter(|v| !covered.contains(v.name.as_str()))
|
|
.map(|v| format_constructor_pattern(v))
|
|
.collect()
|
|
}
|
|
|
|
/// Check Result exhaustiveness
|
|
fn check_result_exhaustiveness(patterns: &[&Pattern]) -> Vec<String> {
|
|
let mut has_ok = false;
|
|
let mut has_err = false;
|
|
|
|
for pattern in patterns {
|
|
if let Pattern::Constructor { name, .. } = pattern {
|
|
match name.name.as_str() {
|
|
"Ok" => has_ok = true,
|
|
"Err" => has_err = true,
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut missing = Vec::new();
|
|
if !has_ok {
|
|
missing.push("Ok(_)".to_string());
|
|
}
|
|
if !has_err {
|
|
missing.push("Err(_)".to_string());
|
|
}
|
|
missing
|
|
}
|
|
|
|
/// Check tuple exhaustiveness
|
|
fn check_tuple_exhaustiveness(
|
|
patterns: &[&Pattern],
|
|
_elements: &[Type],
|
|
_env: &TypeEnv,
|
|
) -> Vec<String> {
|
|
// Tuples need a pattern that matches the whole tuple
|
|
// For simplicity, we just check if there's any tuple pattern
|
|
let has_tuple_pattern = patterns.iter().any(|p| matches!(p, Pattern::Tuple { .. }));
|
|
|
|
if has_tuple_pattern {
|
|
Vec::new()
|
|
} else {
|
|
vec!["(_, ...)".to_string()]
|
|
}
|
|
}
|
|
|
|
/// Format a variant as a pattern suggestion
|
|
fn format_constructor_pattern(variant: &VariantDef) -> String {
|
|
use crate::types::VariantFieldsDef;
|
|
|
|
match &variant.fields {
|
|
VariantFieldsDef::Unit => variant.name.clone(),
|
|
VariantFieldsDef::Tuple(fields) => {
|
|
let wildcards: Vec<&str> = fields.iter().map(|_| "_").collect();
|
|
format!("{}({})", variant.name, wildcards.join(", "))
|
|
}
|
|
VariantFieldsDef::Record(fields) => {
|
|
let wildcards: Vec<String> = fields.iter().map(|(n, _)| format!("{}: _", n)).collect();
|
|
format!("{} {{ {} }}", variant.name, wildcards.join(", "))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Find redundant arms that can never match
|
|
fn find_redundant_arms(patterns: &[&Pattern]) -> Vec<usize> {
|
|
let mut redundant = Vec::new();
|
|
let mut seen_catch_all = false;
|
|
|
|
for (i, pattern) in patterns.iter().enumerate() {
|
|
if seen_catch_all {
|
|
// Any pattern after a catch-all is redundant
|
|
redundant.push(i);
|
|
} else if is_catch_all(pattern) {
|
|
seen_catch_all = true;
|
|
}
|
|
}
|
|
|
|
redundant
|
|
}
|
|
|
|
/// Generate a hint message for missing patterns
|
|
pub fn missing_patterns_hint(missing: &[String]) -> String {
|
|
if missing.is_empty() {
|
|
return String::new();
|
|
}
|
|
|
|
if missing.len() == 1 {
|
|
format!("Pattern '{}' is not covered.", missing[0])
|
|
} else if missing.len() <= 3 {
|
|
format!("Patterns {} are not covered.", missing.join(", "))
|
|
} else {
|
|
format!(
|
|
"Patterns {}, and {} more are not covered.",
|
|
missing[..2].join(", "),
|
|
missing.len() - 2
|
|
)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::ast::{Ident, Span};
|
|
|
|
fn span() -> Span {
|
|
Span::default()
|
|
}
|
|
|
|
fn make_ident(name: &str) -> Ident {
|
|
Ident {
|
|
name: name.to_string(),
|
|
span: span(),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_bool_exhaustive() {
|
|
let patterns = vec![
|
|
Pattern::Literal(Literal {
|
|
kind: LiteralKind::Bool(true),
|
|
span: span(),
|
|
}),
|
|
Pattern::Literal(Literal {
|
|
kind: LiteralKind::Bool(false),
|
|
span: span(),
|
|
}),
|
|
];
|
|
let refs: Vec<&Pattern> = patterns.iter().collect();
|
|
let missing = check_bool_exhaustiveness(&refs);
|
|
assert!(missing.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_bool_missing_true() {
|
|
let patterns = vec![Pattern::Literal(Literal {
|
|
kind: LiteralKind::Bool(false),
|
|
span: span(),
|
|
})];
|
|
let refs: Vec<&Pattern> = patterns.iter().collect();
|
|
let missing = check_bool_exhaustiveness(&refs);
|
|
assert_eq!(missing, vec!["true"]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_option_exhaustive() {
|
|
let patterns = vec![
|
|
Pattern::Constructor {
|
|
name: make_ident("None"),
|
|
fields: vec![],
|
|
span: span(),
|
|
},
|
|
Pattern::Constructor {
|
|
name: make_ident("Some"),
|
|
fields: vec![Pattern::Wildcard(span())],
|
|
span: span(),
|
|
},
|
|
];
|
|
let refs: Vec<&Pattern> = patterns.iter().collect();
|
|
let env = TypeEnv::new();
|
|
let missing = check_option_exhaustiveness(&refs, &Type::Int, &env);
|
|
assert!(missing.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_option_missing_none() {
|
|
let patterns = vec![Pattern::Constructor {
|
|
name: make_ident("Some"),
|
|
fields: vec![Pattern::Wildcard(span())],
|
|
span: span(),
|
|
}];
|
|
let refs: Vec<&Pattern> = patterns.iter().collect();
|
|
let env = TypeEnv::new();
|
|
let missing = check_option_exhaustiveness(&refs, &Type::Int, &env);
|
|
assert!(missing.contains(&"None".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_wildcard_covers_all() {
|
|
let patterns = vec![Pattern::Wildcard(span())];
|
|
let refs: Vec<&Pattern> = patterns.iter().collect();
|
|
assert!(is_catch_all(&patterns[0]));
|
|
|
|
let env = TypeEnv::new();
|
|
let missing = find_missing_patterns(&Type::Bool, &refs, &env);
|
|
assert!(missing.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_redundant_after_wildcard() {
|
|
let patterns = vec![
|
|
Pattern::Wildcard(span()),
|
|
Pattern::Literal(Literal {
|
|
kind: LiteralKind::Bool(true),
|
|
span: span(),
|
|
}),
|
|
];
|
|
let refs: Vec<&Pattern> = patterns.iter().collect();
|
|
let redundant = find_redundant_arms(&refs);
|
|
assert_eq!(redundant, vec![1]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_result_exhaustive() {
|
|
let patterns = vec![
|
|
Pattern::Constructor {
|
|
name: make_ident("Ok"),
|
|
fields: vec![Pattern::Wildcard(span())],
|
|
span: span(),
|
|
},
|
|
Pattern::Constructor {
|
|
name: make_ident("Err"),
|
|
fields: vec![Pattern::Wildcard(span())],
|
|
span: span(),
|
|
},
|
|
];
|
|
let refs: Vec<&Pattern> = patterns.iter().collect();
|
|
let missing = check_result_exhaustiveness(&refs);
|
|
assert!(missing.is_empty());
|
|
}
|
|
}
|