Skip to main content

bop/
check.rs

1//! Static checks that run after parse, before execution.
2//!
3//! Currently the only check is **match exhaustiveness**: if
4//! every arm of a `match` is an enum-variant pattern on the
5//! same enum type and there's no catch-all, we can tell —
6//! from the declared variant list — whether the match covers
7//! them all. Missing variants surface as `BopWarning`s the CLI
8//! prints before running; uncovered variants *still* raise a
9//! "No match arm matched" runtime error if they ever fire, so
10//! the check is advisory rather than load-bearing.
11//!
12//! Kept deliberately narrow:
13//!
14//! - Only enum-shaped matches (all arms `EnumType::Variant`
15//!   with the same outer `EnumType`) are analysed. Literal
16//!   matches and heterogeneous matches are skipped — they'd
17//!   need a different notion of "coverage".
18//! - Guards on arms don't count toward coverage. `Variant(x)
19//!   if x > 0` matches a *subset* of the variant, so the arm
20//!   no longer fully covers `Variant`.
21//!
22//! Imports: `check_program` alone only sees enums declared in
23//! the analysed AST, so `match` arms over an imported enum
24//! would look under-covered. Callers with access to a module
25//! resolver (the CLI has one via `BopHost::resolve_module`)
26//! should use [`check_program_with_resolver`] instead — it
27//! walks every top-level `use` statement, parses the referenced
28//! module's source, and folds its (transitive) enum decls into
29//! the table before running the checks. A resolver that returns
30//! `None` or an error for a given module is treated as a
31//! silent opacity fallback rather than a hard failure; the
32//! checker is advisory.
33
34#[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
50/// Run every static check over `stmts` and collect the
51/// resulting warnings. Never errors — warnings are the only
52/// output.
53///
54/// See [`check_program_with_resolver`] for a variant that
55/// walks `use` statements to pick up imported enum
56/// declarations; this plain version treats imported enums as
57/// opaque and skips exhaustiveness warnings on them.
58pub 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
65/// Like [`check_program`] but follows `use` statements via a
66/// module resolver so `match` arms over imported enums can be
67/// exhaustiveness-checked. `resolver` has the same shape as
68/// `BopHost::resolve_module`:
69///
70/// - `Some(Ok(source))` — module source; parsed + its enums
71///   (transitively) folded into the table.
72/// - `Some(Err(_))` — resolver failed; treated the same as
73///   `None` (check skips the enum; advisory fallback).
74/// - `None` — not our module; skip.
75///
76/// The checker never surfaces a failure from the resolver — it
77/// simply falls back to "imported enums stay opaque" in that
78/// case, the same behaviour [`check_program`] has.
79pub 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
94/// Recursively walk `use` statements and fold each imported
95/// module's enum declarations into `enums`. Silently drops
96/// parse / resolver failures — the checker is advisory, not
97/// load-bearing.
98fn 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                // Already pulled in via a shallower import.
110                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            // Same-name enums already declared in the root win
121            // (first-write-wins). Imported modules should only
122            // *supply* enums the root program doesn't already
123            // know about.
124            for (name, variants) in collect_enum_decls(&imported_stmts) {
125                enums.entry(name).or_insert(variants);
126            }
127            // Recurse so a module's own imports are followed.
128            collect_imported_enum_decls(&imported_stmts, resolver, enums, visited);
129        }
130    }
131}
132
133/// Walk every top-level `enum Foo { ... }` decl in the program
134/// so exhaustiveness checks can consult the variant list
135/// without re-walking the AST per match. Enums nested inside
136/// fn bodies are included too — Bop lets you declare types
137/// anywhere, and a match in a sibling fn can still reach them.
138fn 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        // Declarations, imports, breaks, continues — no sub-expr
236        // to check.
237        _ => {}
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        // Recurse into every sub-expression that could contain
258        // a `match`. This is a bit verbose but avoids visitor
259        // boilerplate; add a variant to `walk_exprs` only if
260        // the recursion list grows much.
261        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        // Literals, identifiers, string interpolation, none —
328        // nothing to recurse into.
329        _ => {}
330    }
331}
332
333/// Core of the check: given a match's arms and the declared
334/// enums, determine whether the match is exhaustive and emit
335/// a warning if not.
336fn check_match_exhaustive(
337    arms: &[MatchArm],
338    enums: &BTreeMap<String, Vec<VariantDecl>>,
339    match_line: u32,
340    warnings: &mut Vec<BopWarning>,
341) {
342    // Step 1: any catch-all arm without a guard makes the
343    // match trivially exhaustive (the fallback always fires).
344    // A guarded catch-all doesn't count — the guard can veto.
345    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    // Step 2: unify the enum under scrutiny. If every
355    // non-guarded arm's *top-level* pattern references the
356    // same `EnumType::*`, that's the enum we can check. If
357    // arms are heterogeneous (literals, structs, arrays, or
358    // two different enums), we bail — no coherent coverage
359    // analysis applies. `target_enum` is owned (rather than a
360    // borrow out of the AST) so the check pass doesn't need to
361    // thread lifetimes through every helper.
362    let mut target_enum: Option<String> = None;
363    let mut covered: Vec<String> = Vec::new();
364    for arm in arms {
365        // Guarded arms narrow their variant (the body only
366        // runs when the guard is truthy) so they don't
367        // contribute to coverage. Unguarded arms do.
368        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        // No enum-variant arm at all — pattern set is entirely
376        // literals / structs / etc., which we can't
377        // exhaustiveness-check at this level.
378        return;
379    };
380    let Some(decl) = enums.get(&enum_name) else {
381        // Enum isn't declared locally — could be imported;
382        // bail rather than warn on a potentially-complete
383        // match we can't verify.
384        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
413/// A pattern that matches every value regardless of shape —
414/// wildcard or a bare binding. Or-patterns made entirely of
415/// catch-alls count too. Everything else is skipped for this
416/// check.
417fn 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
425/// Fold the variants an arm's pattern references into
426/// `covered`. Returns `true` if the arm fits the "all arms
427/// reference the same enum" precondition; `false` to bail the
428/// check entirely. `contributes` is `false` for guarded arms —
429/// we still want to confirm the arm's enum matches, but we
430/// won't count it toward coverage.
431fn gather_variants(
432    pattern: &Pattern,
433    target_enum: &mut Option<String>,
434    covered: &mut Vec<String>,
435    contributes: bool,
436) -> bool {
437    // Catch-all arms can't happen here — the outer scan
438    // returns early on them. A guarded binding *can*, and is
439    // treated as "contributes nothing, but doesn't break the
440    // precondition".
441    match pattern {
442        Pattern::Wildcard | Pattern::Binding(_) => true,
443        Pattern::EnumVariant {
444            type_name,
445            variant,
446            ..
447        } => {
448            // `target_enum` is owned (`Option<String>`) so the
449            // first enum-variant pattern seeds it and every
450            // subsequent arm compares against the stored name.
451            // No lifetimes, no leaks — the extra allocation is
452            // one `String` per analysed match, which is
453            // negligible next to the rest of the check pass.
454            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, // two different enums in one match
469            }
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        // Literal / struct / array patterns on an enum scrutinee
480        // don't fit coverage analysis — bail.
481        _ => 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        // A match that mixes a literal and an enum variant
574        // can't be exhaustiveness-checked by this pass —
575        // returning zero warnings is the correct pragmatic
576        // answer.
577        let src = r#"enum Tag { A, B }
578let _ = match 1 {
579    1 => "one",
580    2 => "two",
581}"#;
582        // This happens to include no enum at all — still zero
583        // warnings. The test locks the "no false positives on
584        // literal matches" invariant.
585        assert!(warnings(src).is_empty());
586    }
587
588    #[test]
589    fn unknown_enum_bails_rather_than_warning() {
590        // `FromAnotherModule` isn't declared here; the check
591        // shouldn't warn (we can't verify coverage).
592        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        // The `match` keyword sits on line 2 of the source.
610        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    // ─── Cross-import exhaustiveness ──────────────────────────────
643
644    /// Helper that wires a tiny `(&str, &str)` module map into
645    /// the resolver closure shape.
646    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        // `use` brings `Shape` in from the `geom` module; the
663        // match under-covers (`Triangle` unhandled), so the
664        // checker — now that it follows the import — fires a
665        // warning naming the missing variant.
666        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        // `a` re-exports via `use b`; the root's match over
705        // the enum declared in `b` should still be checkable.
706        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        // Resolver returns None → the checker treats the
729        // enum as opaque and suppresses the warning rather
730        // than raising a hard error. Matches the advisory
731        // nature of `check_program`.
732        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        // If the root program declares `Color` too, the
746        // root's definition wins (first-write-wins). The
747        // imported enum's variants don't leak into the
748        // check.
749        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}