1#[cfg(feature = "no_std")]
35use alloc::{format, string::String, vec::Vec};
36
37use crate::error::BopWarning;
38use crate::parser::{
39 Expr, ExprKind, MatchArm, Pattern, Stmt, StmtKind, VariantDecl,
40};
41
42#[cfg(feature = "no_std")]
43use alloc_import::collections::{BTreeMap, BTreeSet};
44#[cfg(not(feature = "no_std"))]
45use std::collections::{BTreeMap, BTreeSet};
46
47#[cfg(feature = "no_std")]
48use alloc as alloc_import;
49
50pub fn check_program(stmts: &[Stmt]) -> Vec<BopWarning> {
59 let mut warnings = Vec::new();
60 let enums = collect_enum_decls(stmts);
61 check_stmts(stmts, &enums, &mut warnings);
62 warnings
63}
64
65pub fn check_program_with_resolver<R>(
80 stmts: &[Stmt],
81 resolver: &mut R,
82) -> Vec<BopWarning>
83where
84 R: FnMut(&str) -> Option<Result<String, crate::error::BopError>>,
85{
86 let mut warnings = Vec::new();
87 let mut enums = collect_enum_decls(stmts);
88 let mut visited: BTreeSet<String> = BTreeSet::new();
89 collect_imported_enum_decls(stmts, resolver, &mut enums, &mut visited);
90 check_stmts(stmts, &enums, &mut warnings);
91 warnings
92}
93
94fn collect_imported_enum_decls<R>(
99 stmts: &[Stmt],
100 resolver: &mut R,
101 enums: &mut BTreeMap<String, Vec<VariantDecl>>,
102 visited: &mut BTreeSet<String>,
103) where
104 R: FnMut(&str) -> Option<Result<String, crate::error::BopError>>,
105{
106 for stmt in stmts {
107 if let StmtKind::Use { path, .. } = &stmt.kind {
108 if !visited.insert(path.clone()) {
109 continue;
111 }
112 let source = match resolver(path) {
113 Some(Ok(s)) => s,
114 _ => continue,
115 };
116 let imported_stmts = match crate::parse(&source) {
117 Ok(v) => v,
118 Err(_) => continue,
119 };
120 for (name, variants) in collect_enum_decls(&imported_stmts) {
125 enums.entry(name).or_insert(variants);
126 }
127 collect_imported_enum_decls(&imported_stmts, resolver, enums, visited);
129 }
130 }
131}
132
133fn collect_enum_decls(stmts: &[Stmt]) -> BTreeMap<String, Vec<VariantDecl>> {
139 let mut enums = BTreeMap::new();
140 collect_enum_decls_rec(stmts, &mut enums);
141 enums
142}
143
144fn collect_enum_decls_rec(stmts: &[Stmt], enums: &mut BTreeMap<String, Vec<VariantDecl>>) {
145 for stmt in stmts {
146 match &stmt.kind {
147 StmtKind::EnumDecl { name, variants } => {
148 enums.insert(name.clone(), variants.clone());
149 }
150 StmtKind::FnDecl { body, .. } => {
151 collect_enum_decls_rec(body, enums);
152 }
153 StmtKind::MethodDecl { body, .. } => {
154 collect_enum_decls_rec(body, enums);
155 }
156 StmtKind::If {
157 body,
158 else_ifs,
159 else_body,
160 ..
161 } => {
162 collect_enum_decls_rec(body, enums);
163 for (_, b) in else_ifs {
164 collect_enum_decls_rec(b, enums);
165 }
166 if let Some(eb) = else_body {
167 collect_enum_decls_rec(eb, enums);
168 }
169 }
170 StmtKind::While { body, .. }
171 | StmtKind::Repeat { body, .. }
172 | StmtKind::ForIn { body, .. } => {
173 collect_enum_decls_rec(body, enums);
174 }
175 _ => {}
176 }
177 }
178}
179
180fn check_stmts(
181 stmts: &[Stmt],
182 enums: &BTreeMap<String, Vec<VariantDecl>>,
183 warnings: &mut Vec<BopWarning>,
184) {
185 for stmt in stmts {
186 check_stmt(stmt, enums, warnings);
187 }
188}
189
190fn check_stmt(
191 stmt: &Stmt,
192 enums: &BTreeMap<String, Vec<VariantDecl>>,
193 warnings: &mut Vec<BopWarning>,
194) {
195 match &stmt.kind {
196 StmtKind::Let { value, .. } => check_expr(value, enums, warnings),
197 StmtKind::Assign { value, .. } => check_expr(value, enums, warnings),
198 StmtKind::ExprStmt(expr) => check_expr(expr, enums, warnings),
199 StmtKind::Return { value: Some(expr) } => check_expr(expr, enums, warnings),
200 StmtKind::Return { value: None } => {}
201 StmtKind::If {
202 condition,
203 body,
204 else_ifs,
205 else_body,
206 } => {
207 check_expr(condition, enums, warnings);
208 check_stmts(body, enums, warnings);
209 for (c, b) in else_ifs {
210 check_expr(c, enums, warnings);
211 check_stmts(b, enums, warnings);
212 }
213 if let Some(eb) = else_body {
214 check_stmts(eb, enums, warnings);
215 }
216 }
217 StmtKind::While { condition, body } => {
218 check_expr(condition, enums, warnings);
219 check_stmts(body, enums, warnings);
220 }
221 StmtKind::Repeat { count, body } => {
222 check_expr(count, enums, warnings);
223 check_stmts(body, enums, warnings);
224 }
225 StmtKind::ForIn { iterable, body, .. } => {
226 check_expr(iterable, enums, warnings);
227 check_stmts(body, enums, warnings);
228 }
229 StmtKind::FnDecl { body, .. } => {
230 check_stmts(body, enums, warnings);
231 }
232 StmtKind::MethodDecl { body, .. } => {
233 check_stmts(body, enums, warnings);
234 }
235 _ => {}
238 }
239}
240
241fn check_expr(
242 expr: &Expr,
243 enums: &BTreeMap<String, Vec<VariantDecl>>,
244 warnings: &mut Vec<BopWarning>,
245) {
246 match &expr.kind {
247 ExprKind::Match { scrutinee, arms } => {
248 check_expr(scrutinee, enums, warnings);
249 for arm in arms {
250 if let Some(guard) = &arm.guard {
251 check_expr(guard, enums, warnings);
252 }
253 check_expr(&arm.body, enums, warnings);
254 }
255 check_match_exhaustive(arms, enums, expr.line, warnings);
256 }
257 ExprKind::BinaryOp { left, right, .. } => {
262 check_expr(left, enums, warnings);
263 check_expr(right, enums, warnings);
264 }
265 ExprKind::UnaryOp { expr: e, .. } => check_expr(e, enums, warnings),
266 ExprKind::Call { callee, args } => {
267 check_expr(callee, enums, warnings);
268 for a in args {
269 check_expr(a, enums, warnings);
270 }
271 }
272 ExprKind::MethodCall { object, args, .. } => {
273 check_expr(object, enums, warnings);
274 for a in args {
275 check_expr(a, enums, warnings);
276 }
277 }
278 ExprKind::Index { object, index } => {
279 check_expr(object, enums, warnings);
280 check_expr(index, enums, warnings);
281 }
282 ExprKind::Array(items) => {
283 for item in items {
284 check_expr(item, enums, warnings);
285 }
286 }
287 ExprKind::Dict(entries) => {
288 for (_, v) in entries {
289 check_expr(v, enums, warnings);
290 }
291 }
292 ExprKind::IfExpr {
293 condition,
294 then_expr,
295 else_expr,
296 } => {
297 check_expr(condition, enums, warnings);
298 check_expr(then_expr, enums, warnings);
299 check_expr(else_expr, enums, warnings);
300 }
301 ExprKind::Lambda { body, .. } => {
302 check_stmts(body, enums, warnings);
303 }
304 ExprKind::FieldAccess { object, .. } => check_expr(object, enums, warnings),
305 ExprKind::StructConstruct { fields, .. } => {
306 for (_, v) in fields {
307 check_expr(v, enums, warnings);
308 }
309 }
310 ExprKind::EnumConstruct { payload, .. } => {
311 use crate::parser::VariantPayload;
312 match payload {
313 VariantPayload::Unit => {}
314 VariantPayload::Tuple(args) => {
315 for a in args {
316 check_expr(a, enums, warnings);
317 }
318 }
319 VariantPayload::Struct(fields) => {
320 for (_, v) in fields {
321 check_expr(v, enums, warnings);
322 }
323 }
324 }
325 }
326 ExprKind::Try(inner) => check_expr(inner, enums, warnings),
327 _ => {}
330 }
331}
332
333fn check_match_exhaustive(
337 arms: &[MatchArm],
338 enums: &BTreeMap<String, Vec<VariantDecl>>,
339 match_line: u32,
340 warnings: &mut Vec<BopWarning>,
341) {
342 for arm in arms {
346 if arm.guard.is_some() {
347 continue;
348 }
349 if is_catch_all(&arm.pattern) {
350 return;
351 }
352 }
353
354 let mut target_enum: Option<String> = None;
363 let mut covered: Vec<String> = Vec::new();
364 for arm in arms {
365 let contributes = arm.guard.is_none();
369 if !gather_variants(&arm.pattern, &mut target_enum, &mut covered, contributes) {
370 return;
371 }
372 }
373
374 let Some(enum_name) = target_enum else {
375 return;
379 };
380 let Some(decl) = enums.get(&enum_name) else {
381 return;
385 };
386
387 let missing: Vec<&str> = decl
388 .iter()
389 .filter(|v| !covered.iter().any(|c| c == &v.name))
390 .map(|v| v.name.as_str())
391 .collect();
392 if missing.is_empty() {
393 return;
394 }
395
396 let list = missing.join(", ");
397 let msg = format!(
398 "non-exhaustive `match` on `{}`: missing {}",
399 enum_name,
400 missing
401 .iter()
402 .map(|v| format!("`{}::{}`", enum_name, v))
403 .collect::<Vec<_>>()
404 .join(", "),
405 );
406 let hint = format!(
407 "add an arm for each missing variant, or a `_` catch-all. Missing: {}",
408 list
409 );
410 warnings.push(BopWarning::at(msg, match_line).with_hint(hint));
411}
412
413fn is_catch_all(pattern: &Pattern) -> bool {
418 match pattern {
419 Pattern::Wildcard | Pattern::Binding(_) => true,
420 Pattern::Or(alts) => alts.iter().all(is_catch_all),
421 _ => false,
422 }
423}
424
425fn gather_variants(
432 pattern: &Pattern,
433 target_enum: &mut Option<String>,
434 covered: &mut Vec<String>,
435 contributes: bool,
436) -> bool {
437 match pattern {
442 Pattern::Wildcard | Pattern::Binding(_) => true,
443 Pattern::EnumVariant {
444 type_name,
445 variant,
446 ..
447 } => {
448 match target_enum {
455 None => {
456 *target_enum = Some(type_name.clone());
457 if contributes {
458 covered.push(variant.clone());
459 }
460 true
461 }
462 Some(existing) if existing == type_name => {
463 if contributes {
464 covered.push(variant.clone());
465 }
466 true
467 }
468 _ => false, }
470 }
471 Pattern::Or(alts) => {
472 for alt in alts {
473 if !gather_variants(alt, target_enum, covered, contributes) {
474 return false;
475 }
476 }
477 true
478 }
479 _ => false,
482 }
483}
484
485#[cfg(test)]
486mod tests {
487 use super::*;
488 use crate::parse;
489
490 fn warnings(source: &str) -> Vec<BopWarning> {
491 let stmts = parse(source).unwrap();
492 check_program(&stmts)
493 }
494
495 #[test]
496 fn exhaustive_match_produces_no_warning() {
497 let src = r#"enum Shape { Circle(r), Square(s) }
498fn area(s) {
499 return match s {
500 Shape::Circle(r) => r * r,
501 Shape::Square(s) => s * s,
502 }
503}"#;
504 assert!(warnings(src).is_empty());
505 }
506
507 #[test]
508 fn wildcard_arm_counts_as_exhaustive() {
509 let src = r#"enum Shape { Circle(r), Square(s), Triangle }
510let s = Shape::Circle(5)
511let _ = match s {
512 Shape::Circle(r) => r,
513 _ => 0,
514}"#;
515 assert!(warnings(src).is_empty());
516 }
517
518 #[test]
519 fn bare_binding_arm_counts_as_exhaustive() {
520 let src = r#"enum Shape { Circle(r), Square(s) }
521let s = Shape::Circle(5)
522let _ = match s {
523 Shape::Circle(r) => r,
524 other => 0,
525}"#;
526 assert!(warnings(src).is_empty());
527 }
528
529 #[test]
530 fn missing_variant_warns() {
531 let src = r#"enum Shape { Circle(r), Square(s), Triangle }
532let s = Shape::Circle(5)
533let _ = match s {
534 Shape::Circle(r) => r,
535 Shape::Square(s) => s,
536}"#;
537 let ws = warnings(src);
538 assert_eq!(ws.len(), 1, "expected exactly one warning, got {:?}", ws);
539 assert!(
540 ws[0].message.contains("non-exhaustive"),
541 "msg: {}",
542 ws[0].message
543 );
544 assert!(ws[0].message.contains("`Shape::Triangle`"), "msg: {}", ws[0].message);
545 }
546
547 #[test]
548 fn guarded_arm_does_not_count_toward_coverage() {
549 let src = r#"enum Light { Red, Green }
550let l = Light::Red
551let _ = match l {
552 Light::Red if true => "stop",
553 Light::Green => "go",
554}"#;
555 let ws = warnings(src);
556 assert_eq!(ws.len(), 1, "expected a warning, got {:?}", ws);
557 assert!(ws[0].message.contains("`Light::Red`"));
558 }
559
560 #[test]
561 fn or_pattern_covers_multiple_variants() {
562 let src = r#"enum E { A, B, C }
563let e = E::A
564let _ = match e {
565 E::A | E::B => 1,
566 E::C => 2,
567}"#;
568 assert!(warnings(src).is_empty());
569 }
570
571 #[test]
572 fn heterogeneous_match_skips_check() {
573 let src = r#"enum Tag { A, B }
578let _ = match 1 {
579 1 => "one",
580 2 => "two",
581}"#;
582 assert!(warnings(src).is_empty());
586 }
587
588 #[test]
589 fn unknown_enum_bails_rather_than_warning() {
590 let src = r#"fn handle(x) {
593 return match x {
594 FromAnotherModule::A => 1,
595 FromAnotherModule::B => 2,
596 }
597}"#;
598 assert!(warnings(src).is_empty());
599 }
600
601 #[test]
602 fn warning_carries_match_line() {
603 let src = r#"enum E { A, B }
604let _ = match E::A {
605 E::A => 1,
606}"#;
607 let ws = warnings(src);
608 assert_eq!(ws.len(), 1);
609 assert_eq!(ws[0].line, Some(2));
611 }
612
613 #[test]
614 fn match_inside_fn_body_is_checked() {
615 let src = r#"enum E { A, B, C }
616fn pick(e) {
617 return match e {
618 E::A => 1,
619 E::B => 2,
620 }
621}"#;
622 let ws = warnings(src);
623 assert_eq!(ws.len(), 1);
624 assert!(ws[0].message.contains("`E::C`"));
625 }
626
627 #[test]
628 fn match_inside_if_branch_is_checked() {
629 let src = r#"enum E { A, B, C }
630let e = E::A
631if true {
632 let _ = match e {
633 E::A => 1,
634 E::B => 2,
635 }
636}"#;
637 let ws = warnings(src);
638 assert_eq!(ws.len(), 1);
639 assert!(ws[0].message.contains("`E::C`"));
640 }
641
642 fn warnings_with_modules(
647 source: &str,
648 modules: &[(&str, &str)],
649 ) -> Vec<BopWarning> {
650 let stmts = parse(source).unwrap();
651 let mut resolver = |name: &str| -> Option<Result<String, crate::error::BopError>> {
652 modules
653 .iter()
654 .find(|(n, _)| *n == name)
655 .map(|(_, src)| Ok(String::from(*src)))
656 };
657 check_program_with_resolver(&stmts, &mut resolver)
658 }
659
660 #[test]
661 fn imported_enum_missing_variant_warns_via_resolver() {
662 let ws = warnings_with_modules(
667 r#"use geom
668let s = Shape::Circle(5)
669let _ = match s {
670 Shape::Circle(r) => r,
671 Shape::Square(s) => s,
672}"#,
673 &[("geom", "enum Shape { Circle(r), Square(s), Triangle }")],
674 );
675 assert_eq!(ws.len(), 1);
676 assert!(
677 ws[0].message.contains("`Shape::Triangle`"),
678 "got: {}",
679 ws[0].message
680 );
681 }
682
683 #[test]
684 fn imported_enum_exhaustive_match_produces_no_warning_via_resolver() {
685 let ws = warnings_with_modules(
686 r#"use geom
687let s = Shape::Circle(5)
688let _ = match s {
689 Shape::Circle(r) => r,
690 Shape::Square(s) => s,
691 Shape::Triangle => 0,
692}"#,
693 &[("geom", "enum Shape { Circle(r), Square(s), Triangle }")],
694 );
695 assert!(
696 ws.is_empty(),
697 "expected no warnings when all variants covered, got: {:?}",
698 ws
699 );
700 }
701
702 #[test]
703 fn transitive_imported_enum_is_picked_up() {
704 let ws = warnings_with_modules(
707 r#"use a
708let c = Color::Red
709let _ = match c {
710 Color::Red => "r",
711 Color::Blue => "b",
712}"#,
713 &[
714 ("a", "use b"),
715 ("b", "enum Color { Red, Blue, Green }"),
716 ],
717 );
718 assert_eq!(ws.len(), 1);
719 assert!(
720 ws[0].message.contains("`Color::Green`"),
721 "got: {}",
722 ws[0].message
723 );
724 }
725
726 #[test]
727 fn unresolvable_module_is_silently_skipped() {
728 let ws = warnings_with_modules(
733 r#"use missing
734let c = Color::Red
735let _ = match c {
736 Color::Red => 1,
737}"#,
738 &[],
739 );
740 assert!(ws.is_empty());
741 }
742
743 #[test]
744 fn root_enum_shadows_imported_same_name() {
745 let ws = warnings_with_modules(
750 r#"use paint
751enum Color { Red, Blue }
752let c = Color::Red
753let _ = match c {
754 Color::Red => 1,
755 Color::Blue => 2,
756}"#,
757 &[("paint", "enum Color { Red, Green, Yellow }")],
758 );
759 assert!(
760 ws.is_empty(),
761 "expected no warning: root's Color is fully covered, got: {:?}",
762 ws
763 );
764 }
765}