Repository: gleam-lang/gleam (rust)

Issue:
Print correct syntax for "Inexhaustive patterns" diagnostic, relative to current module
<!--
Gleam is post-v1, so breaking changes are not possible.

Any new addition to the language itself needs to enable something that is not possible in Gleam today, and solve existing pain-points.

For anything other than trivial and uncontroversial additions please make a GitHub discussion before making an issue. https://github.com/gleam-lang/gleam/discussions/categories/ideas-suggestions
-->
At the moment, this doesn't use the current environment, and just uses type names. The first step to implementing #2957 is to make the error message use the environment to print types

Pull request description:
Print module names for "inexhaustive case expression" error
Closes #3388
The error reporting code now uses the current environment to print module names.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cce16d7ab..0c2c8e2f2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,15 @@
 
   ([Giacomo Cavalieri](https://github.com/giacomocavalieri))
 
+- In inexhaustive pattern match errors the missing variants variants are now
+  printed using the correct syntax for the module the error is emitted in,
+  rather than the module it was defined in.
+
+  For example, if the variant would need be qualified by the name of the
+  defining module then that would be shown. If the variant was aliased when it
+  was imported then the alias would be shown.
+  ([Surya Rose](https://github.com/gearsdatapacks))
+
 ### Formatter
 
 ### Language Server
diff --git a/compiler-core/src/analyse.rs b/compiler-core/src/analyse.rs
index df96e4e11..75d2003ae 100644
--- a/compiler-core/src/analyse.rs
+++ b/compiler-core/src/analyse.rs
@@ -968,6 +968,12 @@ impl<'a, A> ModuleAnalyzer<'a, A> {
                 value_constructor_publicity,
                 deprecation.clone(),
             );
+
+            environment.value_names.named_constructor_in_scope(
+                environment.current_module.clone(),
+                constructor.name.clone(),
+                constructor.name.clone(),
+            );
         }
 
         // Now record the constructors for the type.
diff --git a/compiler-core/src/analyse/imports.rs b/compiler-core/src/analyse/imports.rs
index ca9d09ad8..d7f2de567 100644
--- a/compiler-core/src/analyse/imports.rs
+++ b/compiler-core/src/analyse/imports.rs
@@ -139,12 +139,19 @@ impl<'context, 'problems> Importer<'context, 'problems> {
         };
 
         match variant {
-            &ValueConstructorVariant::Record { .. } => self.environment.init_usage(
-                used_name.clone(),
-                EntityKind::ImportedConstructor,
-                location,
-                self.problems,
-            ),
+            ValueConstructorVariant::Record { name, module, .. } => {
+                self.environment.init_usage(
+                    used_name.clone(),
+                    EntityKind::ImportedConstructor,
+                    location,
+                    self.problems,
+                );
+                self.environment.value_names.named_constructor_in_scope(
+                    module.clone(),
+                    name.clone(),
+                    used_name.clone(),
+                );
+            }
             _ => self.environment.init_usage(
                 used_name.clone(),
                 EntityKind::ImportedValue,
@@ -225,7 +232,11 @@ impl<'context, 'problems> Importer<'context, 'problems> {
             let _ = self
                 .environment
                 .imported_modules
-                .insert(used_name, (import.location, import_info));
+                .insert(used_name.clone(), (import.location, import_info));
+
+            self.environment
+                .value_names
+                .imported_module(import.module.clone(), used_name)
         };
 
         Ok(())
diff --git a/compiler-core/src/exhaustiveness.rs b/compiler-core/src/exhaustiveness.rs
index 3c2028e5f..3cd18baa4 100644
--- a/compiler-core/src/exhaustiveness.rs
+++ b/compiler-core/src/exhaustiveness.rs
@@ -32,6 +32,7 @@ mod missing_patterns;
 mod pattern;
 #[cfg(test)]
 mod pattern_tests;
+pub mod printer;
 
 use self::pattern::{Constructor, Pattern, PatternId};
 use crate::{
diff --git a/compiler-core/src/exhaustiveness/missing_patterns.rs b/compiler-core/src/exhaustiveness/missing_patterns.rs
index 621201e86..c1037d09e 100644
--- a/compiler-core/src/exhaustiveness/missing_patterns.rs
+++ b/compiler-core/src/exhaustiveness/missing_patterns.rs
@@ -1,7 +1,6 @@
-use super::{Constructor, Decision, Match, Variable};
+use super::{printer::Printer, Constructor, Decision, Match, Variable};
 use crate::type_::environment::Environment;
 use ecow::EcoString;
-use itertools::Itertools;
 use std::collections::{HashMap, HashSet};
 
 /// Returns a list of patterns not covered by the match expression.
@@ -21,10 +20,15 @@ pub fn missing_patterns(matches: &Match, environment: &Environment<'_>) -> Vec<E
 /// Information about a single constructor/value (aka term) being tested, used
 /// to build a list of names of missing patterns.
 #[derive(Debug)]
-enum Term {
+pub enum Term {
     Variant {
         variable: Variable,
         name: EcoString,
+        module: EcoString,
+        arguments: Vec<Variable>,
+    },
+    Tuple {
+        variable: Variable,
         arguments: Vec<Variable>,
     },
     Infinite {
@@ -41,82 +45,15 @@ enum Term {
 }
 
 impl Term {
-    fn variable(&self) -> &Variable {
+    pub fn variable(&self) -> &Variable {
         match self {
             Term::Variant { variable, .. } => variable,
+            Term::Tuple { variable, .. } => variable,
             Term::Infinite { variable } => variable,
             Term::EmptyList { variable } => variable,
             Term::List { variable, .. } => variable,
         }
     }
-
-    fn pattern_string(&self, terms: &[Term], mapping: &HashMap<usize, usize>) -> EcoString {
-        match self {
-            Term::Variant {
-                name, arguments, ..
-            } => {
-                if arguments.is_empty() {
-                    return name.clone();
-                }
-                let args = arguments
-                    .iter()
-                    .map(|variable| {
-                        mapping
-                            .get(&variable.id)
-                            .map(|&idx| {
-                                terms
-                                    .get(idx)
-                                    .expect("Term must exist")
-                                    .pattern_string(terms, mapping)
-                            })
-                            .unwrap_or_else(|| "_".into())
-                    })
-                    .join(", ");
-                format!("{}({})", name, args).into()
-            }
-
-            Term::Infinite { .. } => "_".into(),
-
-            Term::EmptyList { .. } => "[]".into(),
-
-            Term::List { .. } => format!("[{}]", self.list_pattern_string(terms, mapping)).into(),
-        }
-    }
-
-    fn list_pattern_string(&self, terms: &[Term], mapping: &HashMap<usize, usize>) -> EcoString {
-        match self {
-            Term::Infinite { .. } | Term::Variant { .. } => "_".into(),
-
-            Term::EmptyList { .. } => "".into(),
-
-            Term::List { first, rest, .. } => {
-                let first = mapping
-                    .get(&first.id)
-                    .map(|&idx| {
-                        terms
-                            .get(idx)
-                            .expect("Term must exist")
-                            .pattern_string(terms, mapping)
-                    })
-                    .unwrap_or_else(|| "_".into());
-                let rest = mapping
-                    .get(&rest.id)
-                    .map(|&idx| {
-                        terms
-                            .get(idx)
-                            .expect("Term must exist")
-                            .list_pattern_string(terms, mapping)
-                    })
-                    .unwrap_or_else(|| "_".into());
-
-                match rest.as_str() {
-                    "" => first,
-                    "_" => format!("{first}, ..").into(),
-                    _ => format!("{first}, {rest}").into(),
-                }
-            }
-        }
-    }
 }
 
 fn add_missing_patterns(
@@ -125,6 +62,7 @@ fn add_missing_patterns(
     missing: &mut HashSet<EcoString>,
     environment: &Environment<'_>,
 ) {
+    let mut printer = Printer::new(&environment.value_names);
     match node {
         Decision::Success(_) => {}
 
@@ -150,7 +88,7 @@ fn add_missing_patterns(
 
             let name = terms
                 .first()
-                .map(|term| term.pattern_string(terms, &mapping))
+                .map(|term| printer.print_term(term, terms, &mapping))
                 .unwrap_or_else(|| "_".into());
 
             _ = missing.insert(name);
@@ -175,9 +113,8 @@ fn add_missing_patterns(
 
                     Constructor::Tuple(_) => {
                         let arguments = case.arguments.clone();
-                        terms.push(Term::Variant {
+                        terms.push(Term::Tuple {
                             variable: variable.clone(),
-                            name: "#".into(),
                             arguments,
                         });
                     }
@@ -196,6 +133,7 @@ fn add_missing_patterns(
                         terms.push(Term::Variant {
                             variable: variable.clone(),
                             name,
+                            module,
                             arguments: case.arguments.clone(),
                         });
                     }
diff --git a/compiler-core/src/exhaustiveness/printer.rs b/compiler-core/src/exhaustiveness/printer.rs
new file mode 100644
index 000000000..0b7a1c466
--- /dev/null
+++ b/compiler-core/src/exhaustiveness/printer.rs
@@ -0,0 +1,477 @@
+use std::collections::HashMap;
+
+use ecow::EcoString;
+
+use super::missing_patterns::Term;
+
+#[derive(Debug, Default)]
+pub struct ValueNames {
+    /// Mapping of imported modules to their locally used named
+    ///
+    /// key:   The name of the module
+    /// value: The name the module is aliased to
+    ///
+    /// # Example 1
+    ///
+    /// ```gleam
+    /// import mod1 as my_mod
+    /// ```
+    /// would result in:
+    /// - key:   "mod1"
+    /// - value: "my_mod"
+    ///
+    /// # Example 2
+    ///
+    /// ```gleam
+    /// import mod1
+    /// ```
+    /// would result in:
+    /// - key:   "mod1"
+    /// - value: "mod1"
+    ///
+    imported_modules: HashMap<EcoString, EcoString>,
+
+    /// Constructors which are imported in the current module in an
+    /// unqualified fashion.
+    ///
+    /// key:   (Defining module name, type name)
+    /// value: Alias name
+    ///
+    /// # Example 1
+    ///
+    /// ```gleam
+    /// import wibble.{Wobble}
+    /// ```
+    /// would result in
+    /// - key:   `("wibble", "Wobble")`
+    /// - value: `"Wobble"`
+    ///
+    /// # Example 2
+    ///
+    /// ```gleam
+    /// import wibble.{Wobble as Woo}
+    /// ```
+    /// would result in
+    /// - key:   `("wibble", "Wobble")`
+    /// - value: `"Woo"`
+    ///
+    local_constructors: HashMap<(EcoString, EcoString), EcoString>,
+
+    /// A map from local constructor names to the modules which they refer to.
+    /// This helps resolve cases like:
+    /// ```gleam
+    /// import wibble.{Wobble}
+    /// type Wibble { Wobble }
+    /// ```
+    /// Here, `Wobble` is shadowed, causing `Wobble` not to be valid syntax
+    /// for `wibble.Wobble`.
+    ///
+    /// Each key is the local name of the constructor, and the value is the module
+    /// for which the unqualified version is valid. In the above example,
+    /// it would result in
+    /// - key:   `"Wobble"`
+    /// - value: `"module"` (Whatever the current module is)
+    ///
+    /// But in this case:
+    /// ```gleam
+    /// import wibble.{Wobble as Wubble}
+    /// type Wibble { Wobble }
+    /// ```
+    /// No shadowing occurs, so this isn't needed.
+    ///
+    constructor_names: HashMap<EcoString, EcoString>,
+}
+
+impl ValueNames {
+    pub fn new() -> Self {
+        Self {
+            local_constructors: Default::default(),
+            imported_modules: Default::default(),
+            constructor_names: Default::default(),
+        }
+    }
+
+    /// Record a named value in this module.
+    pub fn named_constructor_in_scope(
+        &mut self,
+        module_name: EcoString,
+        value_name: EcoString,
+        local_alias: EcoString,
+    ) {
+        _ = self
+            .local_constructors
+            .insert((module_name.clone(), value_name), local_alias.clone());
+        _ = self.constructor_names.insert(local_alias, module_name);
+    }
+
+    /// Record an imported module in this module.
+    pub fn imported_module(&mut self, module_name: EcoString, module_alias: EcoString) {
+        _ = self.imported_modules.insert(module_name, module_alias)
+    }
+
+    /// Get the name and optional module qualifier for a named constructor.
+    pub fn named_constructor<'a>(
+        &'a self,
+        module: &'a EcoString,
+        name: &'a EcoString,
+    ) -> NamedValueNames<'a> {
+        let key: (EcoString, EcoString) = (module.clone(), name.clone());
+
+        // There is a local name for this value, use that.
+        if let Some(name) = self.local_constructors.get(&key) {
+            // Only return unqualified syntax if the constructor is not shadowed,
+            // and unqualified syntax is valid.
+            if self
+                .constructor_names
+                .get(name)
+                .expect("Constructors must be added to both maps")
+                == module
+            {
+                return NamedValueNames::Unqualified(name.as_str());
+            }
+        }
+
+        // This value is from a module that has been imported
+        if let Some(module) = self.imported_modules.get(module) {
+            return NamedValueNames::Qualified(module, name.as_str());
+        };
+
+        NamedValueNames::Unimported(name.as_str())
+    }
+}
+
+#[derive(Debug)]
+pub enum NamedValueNames<'a> {
+    /// This value is from a module that has not been imported in this module.
+    Unimported(&'a str),
+    /// This value has been imported in an unqualified fashion in this module.
+    Unqualified(&'a str),
+    /// This value is from a module that has been imported.
+    Qualified(&'a str, &'a str),
+}
+
+#[derive(Debug)]
+pub struct Printer<'a> {
+    names: &'a ValueNames,
+}
+
+impl<'a> Printer<'a> {
+    pub fn new(names: &'a ValueNames) -> Self {
+        Printer { names }
+    }
+
+    pub fn print_term(
+        &mut self,
+        term: &Term,
+        terms: &[Term],
+        mapping: &HashMap<usize, usize>,
+    ) -> EcoString {
+        let mut buffer = EcoString::new();
+        self.print(term, terms, mapping, &mut buffer);
+        buffer
+    }
+
+    fn print(
+        &mut self,
+        term: &Term,
+        terms: &[Term],
+        mapping: &HashMap<usize, usize>,
+        buffer: &mut EcoString,
+    ) {
+        match term {
+            Term::Variant {
+                name,
+                module,
+                arguments,
+                ..
+            } => {
+                let (module, name) = match self.names.named_constructor(module, name) {
+                    NamedValueNames::Qualified(m, n) => (Some(m), n),
+                    NamedValueNames::Unqualified(n) => (None, n),
+                    NamedValueNames::Unimported(n) => {
+                        (Some(module.split('/').last().unwrap_or(module)), n)
+                    }
+                };
+
+                if let Some(module) = module {
+                    buffer.push_str(module);
+                    buffer.push('.');
+                }
+                buffer.push_str(name);
+
+                if arguments.is_empty() {
+                    return;
+                }
+                buffer.push('(');
+                for (i, variable) in arguments.iter().enumerate() {
+                    if i != 0 {
+                        buffer.push_str(", ");
+                    }
+
+                    if let Some(&idx) = mapping.get(&variable.id) {
+                        self.print(
+                            terms.get(idx).expect("Term must exist"),
+                            terms,
+                            mapping,
+                            buffer,
+                        );
+                    } else {
+                        buffer.push('_');
+                    }
+                }
+                buffer.push(')');
+            }
+            Term::Tuple { arguments, .. } => {
+                buffer.push_str("#(");
+                for (i, variable) in arguments.iter().enumerate() {
+                    if i != 0 {
+                        buffer.push_str(", ");
+                    }
+
+                    if let Some(&idx) = mapping.get(&variable.id) {
+                        self.print(
+                            terms.get(idx).expect("Term must exist"),
+                            terms,
+                            mapping,
+                            buffer,
+                        );
+                    } else {
+                        buffer.push('_');
+                    }
+                }
+                buffer.push(')');
+            }
+            Term::Infinite { .. } => buffer.push('_'),
+            Term::EmptyList { .. } => buffer.push_str("[]"),
+            Term::List { .. } => {
+                buffer.push('[');
+                self.print_list(term, terms, mapping, buffer);
+                buffer.push(']');
+            }
+        }
+    }
+
+    fn print_list(
+        &mut self,
+        term: &Term,
+        terms: &[Term],
+        mapping: &HashMap<usize, usize>,
+        buffer: &mut EcoString,
+    ) {
+        match term {
+            Term::Infinite { .. } | Term::Variant { .. } | Term::Tuple { .. } => buffer.push('_'),
+
+            Term::EmptyList { .. } => {}
+
+            Term::List { first, rest, .. } => {
+                if let Some(&idx) = mapping.get(&first.id) {
+                    self.print(
+                        terms.get(idx).expect("Term must exist"),
+                        terms,
+                        mapping,
+                        buffer,
+                    )
+                } else {
+                    buffer.push('_');
+                }
+
+                if let Some(&idx) = mapping.get(&rest.id) {
+                    let term = terms.get(idx).expect("Term must exist");
+
+                    match term {
+                        Term::EmptyList { .. } => {}
+                        _ => {
+                            buffer.push_str(", ");
+                            self.print_list(term, terms, mapping, buffer)
+                        }
+                    }
+                } else {
+                    buffer.push_str(", ..");
+                }
+            }
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::{Printer, ValueNames};
+    use std::{collections::HashMap, sync::Arc};
+
+    use crate::{
+        exhaustiveness::{missing_patterns::Term, Variable},
+        type_::Type,
+    };
+
+    /// Create a variable with a dummy type, for ease of writing tests
+    fn make_variable(id: usize) -> Variable {
+        Variable {
+            id,
+            type_: Arc::new(Type::Tuple { elems: Vec::new() }),
+        }
+    }
+
+    fn get_terms_and_mapping(terms: Vec<Term>) -> (Vec<Term>, HashMap<usize, usize>) {
+        let mut mapping: HashMap<usize, usize> = HashMap::new();
+
+        for (index, term) in terms.iter().enumerate() {
+            _ = mapping.insert(term.variable().id, index);
+        }
+        (terms, mapping)
+    }
+
+    #[test]
+    fn test_value_in_current_module() {
+        let mut names = ValueNames::new();
+
+        names.named_constructor_in_scope("module".into(), "Wibble".into(), "Wibble".into());
+
+        let mut printer = Printer::new(&mut names);
+
+        let term = Term::Variant {
+            variable: make_variable(0),
+            name: "Wibble".into(),
+            module: "module".into(),
+            arguments: Vec::new(),
+        };
+
+        assert_eq!(
+            printer.print_term(&term, &Vec::new(), &HashMap::new()),
+            "Wibble"
+        );
+    }
+
+    #[test]
+    fn test_value_in_current_module_with_arguments() {
+        let mut names = ValueNames::new();
+
+        names.named_constructor_in_scope("module".into(), "Wibble".into(), "Wibble".into());
+
+        let mut printer = Printer::new(&mut names);
+
+        let var1 = make_variable(1);
+
+        let var2 = make_variable(2);
+
+        let term = Term::Variant {
+            variable: make_variable(0),
+            name: "Wibble".into(),
+            module: "module".into(),
+            arguments: vec![var1.clone(), var2.clone()],
+        };
+
+        let (terms, mapping) = get_terms_and_mapping(vec![
+            Term::EmptyList { variable: var1 },
+            Term::Infinite { variable: var2 },
+        ]);
+
+        assert_eq!(printer.print_term(&term, &terms, &mapping), "Wibble([], _)");
+    }
+
+    #[test]
+    fn test_module_alias() {
+        let mut names = ValueNames::new();
+
+        names.imported_module("mod".into(), "shapes".into());
+
+        let mut printer = Printer::new(&mut names);
+
+        let term = Term::Variant {
+            variable: make_variable(0),
+            name: "Rectangle".into(),
+            module: "mod".into(),
+            arguments: Vec::new(),
+        };
+
+        assert_eq!(
+            printer.print_term(&term, &Vec::new(), &HashMap::new()),
+            "shapes.Rectangle"
+        );
+    }
+
+    #[test]
+    fn test_unqualified_value() {
+        let mut names = ValueNames::new();
+
+        names.named_constructor_in_scope("regex".into(), "Regex".into(), "Regex".into());
+
+        let mut printer = Printer::new(&mut names);
+
+        let arg = make_variable(1);
+
+        let term = Term::Variant {
+            variable: make_variable(0),
+            name: "Regex".into(),
+            module: "regex".into(),
+            arguments: vec![arg.clone()],
+        };
+
+        let (terms, mapping) = get_terms_and_mapping(vec![Term::Infinite { variable: arg }]);
+
+        assert_eq!(printer.print_term(&term, &terms, &mapping), "Regex(_)");
+    }
+
+    #[test]
+    fn test_unqualified_value_with_alias() {
+        let mut names = ValueNames::new();
+
+        names.named_constructor_in_scope("regex".into(), "Regex".into(), "Reg".into());
+        names.named_constructor_in_scope("gleam".into(), "None".into(), "None".into());
+
+        let mut printer = Printer::new(&mut names);
+
+        let arg = make_variable(1);
+
+        let term = Term::Variant {
+            variable: make_variable(0),
+            name: "Regex".into(),
+            module: "regex".into(),
+            arguments: vec![arg.clone()],
+        };
+
+        let (terms, mapping) = get_terms_and_mapping(vec![Term::Variant {
+            variable: arg,
+            name: "None".into(),
+            module: "gleam".into(),
+            arguments: vec![],
+        }]);
+
+        assert_eq!(printer.print_term(&term, &terms, &mapping), "Reg(None)");
+    }
+
+    #[test]
+    fn test_list_pattern() {
+        let mut names = ValueNames::new();
+
+        names.named_constructor_in_scope("module".into(), "Type".into(), "Type".into());
+
+        let mut printer = Printer::new(&mut names);
+
+        let var1 = make_variable(1);
+        let var2 = make_variable(2);
+        let var3 = make_variable(3);
+
+        let term = Term::List {
+            variable: make_variable(0),
+            first: var1.clone(),
+            rest: var2.clone(),
+        };
+
+        let (terms, mapping) = get_terms_and_mapping(vec![
+            Term::Variant {
+                variable: var1,
+                name: "Type".into(),
+                module: "module".into(),
+                arguments: Vec::new(),
+            },
+            Term::List {
+                variable: var2,
+                first: var3.clone(),
+                rest: make_variable(0),
+            },
+            Term::Infinite { variable: var3 },
+        ]);
+
+        assert_eq!(printer.print_term(&term, &terms, &mapping), "[Type, _, ..]");
+    }
+}
diff --git a/compiler-core/src/type_/environment.rs b/compiler-core/src/type_/environment.rs
index 6505e8667..319101ef2 100644
--- a/compiler-core/src/type_/environment.rs
+++ b/compiler-core/src/type_/environment.rs
@@ -2,6 +2,7 @@ use crate::{
     analyse::TargetSupport,
     ast::{Publicity, PIPE_VARIABLE},
     build::Target,
+    exhaustiveness::printer::ValueNames,
     uid::UniqueIdGenerator,
 };
 
@@ -54,6 +55,8 @@ pub struct Environment<'a> {
     /// Used to determine if all functions/constants need to support the current
     /// compilation target.
     pub target_support: TargetSupport,
+
+    pub value_names: ValueNames,
 }
 
 impl<'a> Environment<'a> {
@@ -68,6 +71,16 @@ impl<'a> Environment<'a> {
         let prelude = importable_modules
             .get(PRELUDE_MODULE_NAME)
             .expect("Unable to find prelude in importable modules");
+
+        let mut value_names = ValueNames::new();
+        for name in prelude.values.keys() {
+            value_names.named_constructor_in_scope(
+                PRELUDE_MODULE_NAME.into(),
+                name.clone(),
+                name.clone(),
+            );
+        }
+
         Self {
             current_package: current_package.clone(),
             previous_id: ids.next(),
@@ -88,6 +101,7 @@ impl<'a> Environment<'a> {
             current_module,
             entity_usages: vec![HashMap::new()],
             target_support,
+            value_names,
         }
     }
 }


Test patch:
diff --git a/compiler-core/src/type_/tests/exhaustiveness.rs b/compiler-core/src/type_/tests/exhaustiveness.rs
index ab4445688..793c652a4 100644
--- a/compiler-core/src/type_/tests/exhaustiveness.rs
+++ b/compiler-core/src/type_/tests/exhaustiveness.rs
@@ -1,4 +1,4 @@
-use crate::{assert_module_error, assert_no_warnings, assert_warning};
+use crate::{assert_module_error, assert_no_warnings, assert_warning, assert_with_module_error};
 
 #[test]
 fn whatever() {
@@ -986,3 +986,146 @@ pub fn main(wibble) {
 "
     );
 }
+
+#[test]
+fn case_error_prints_module_names() {
+    assert_with_module_error!(
+        ("wibble", "pub type Wibble { Wibble Wobble }"),
+        "
+import wibble
+pub type Things { Thing1 Thing2(Int) }
+pub fn main() {
+    let wobble_thing = #(wibble.Wobble, Thing2(23))
+    case wobble_thing {
+        #(wibble.Wibble, Thing1) -> Nil
+    }
+}
+",
+    );
+}
+
+#[test]
+fn case_error_prints_module_alias() {
+    assert_with_module_error!(
+        ("wibble", "pub type Wibble { Wibble Wobble }"),
+        "
+import wibble as wobble
+pub fn main() {
+    case wobble.Wobble {
+        wobble.Wibble -> Nil
+    }
+}
+",
+    );
+}
+
+#[test]
+fn case_error_prints_unqualified_value() {
+    assert_with_module_error!(
+        ("wibble", "pub type Wibble { Wibble Wobble }"),
+        "
+import wibble.{Wibble, Wobble}
+pub fn main() {
+    case Wobble {
+        Wibble -> Nil
+    }
+}
+",
+    );
+}
+
+#[test]
+fn case_error_prints_aliased_unqualified_value() {
+    assert_with_module_error!(
+        ("wibble", "pub type Wibble { Wibble Wobble }"),
+        "
+import wibble.{Wibble, Wobble as Wubble}
+pub fn main() {
+    case Wibble {
+        Wibble -> Nil
+    }
+}
+",
+    );
+}
+
+#[test]
+fn case_error_prints_prelude_module_unqualified() {
+    assert_module_error!(
+        "
+pub fn main() {
+  let result = Ok(Nil)
+  case result {
+    Ok(Nil) -> Nil
+  }
+}
+"
+    );
+}
+
+#[test]
+fn case_error_prints_prelude_module_when_shadowed() {
+    assert_module_error!(
+        "
+import gleam
+type MyResult { Ok Error }
+pub fn main() {
+  let res = gleam.Ok(10)
+  case res {
+    gleam.Ok(n) -> Nil
+  }
+}
+"
+    );
+}
+
+#[test]
+fn case_error_prints_module_when_shadowed() {
+    assert_with_module_error!(
+        ("mod", "pub type Wibble { Wibble Wobble }"),
+        "
+import mod.{Wibble}
+type Wibble { Wibble Wobble }
+pub fn main() {
+  let wibble = mod.Wibble
+  case wibble {
+    mod.Wobble -> Nil
+  }
+}
+"
+    );
+}
+
+#[test]
+fn case_error_prints_module_when_aliased_and_shadowed() {
+    assert_with_module_error!(
+        ("mod", "pub type Wibble { Wibble Wobble }"),
+        "
+import mod.{Wibble as Wobble}
+type Wibble { Wobble Wubble }
+pub fn main() {
+  let wibble = mod.Wibble
+  case wibble {
+    mod.Wobble -> Nil
+  }
+}
+"
+    );
+}
+
+#[test]
+fn case_error_prints_unqualifed_when_aliased() {
+    assert_with_module_error!(
+        ("mod", "pub type Wibble { Wibble Wobble }"),
+        "
+import mod.{Wibble as Wobble}
+type Wibble { Wibble Wubble }
+pub fn main() {
+  let wibble = mod.Wibble
+  case wibble {
+    mod.Wobble -> Nil
+  }
+}
+"
+    );
+}
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__case_error_prints_aliased_unqualified_value.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__case_error_prints_aliased_unqualified_value.snap
new file mode 100644
index 000000000..89f928dc6
--- /dev/null
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__case_error_prints_aliased_unqualified_value.snap
@@ -0,0 +1,18 @@
+---
+source: compiler-core/src/type_/tests/exhaustiveness.rs
+expression: "\nimport wibble.{Wibble, Wobble as Wubble}\npub fn main() {\n    case Wibble {\n        Wibble -> Nil\n    }\n}\n"
+---
+error: Inexhaustive patterns
+  ┌─ /src/one/two.gleam:4:5
+  │  
+4 │ ╭     case Wibble {
+5 │ │         Wibble -> Nil
+6 │ │     }
+  │ ╰─────^
+
+This case expression does not have a pattern for all possible values.
+If it is run on one of the values without a pattern then it will crash.
+
+The missing patterns are:
+
+    Wubble
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__case_error_prints_module_alias.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__case_error_prints_module_alias.snap
new file mode 100644
index 000000000..75b687d3e
--- /dev/null
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__case_error_prints_module_alias.snap
@@ -0,0 +1,18 @@
+---
+source: compiler-core/src/type_/tests/exhaustiveness.rs
+expression: "\nimport wibble as wobble\npub fn main() {\n    case wobble.Wobble {\n        wobble.Wibble -> Nil\n    }\n}\n"
+---
+error: Inexhaustive patterns
+  ┌─ /src/one/two.gleam:4:5
+  │  
+4 │ ╭     case wobble.Wobble {
+5 │ │         wobble.Wibble -> Nil
+6 │ │     }
+  │ ╰─────^
+
+This case expression does not have a pattern for all possible values.
+If it is run on one of the values without a pattern then it will crash.
+
+The missing patterns are:
+
+    wobble.Wobble
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__case_error_prints_module_names.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__case_error_prints_module_names.snap
new file mode 100644
index 000000000..0edb5a4c7
--- /dev/null
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__case_error_prints_module_names.snap
@@ -0,0 +1,19 @@
+---
+source: compiler-core/src/type_/tests/exhaustiveness.rs
+expression: "\nimport wibble\npub type Things { Thing1 Thing2(Int) }\npub fn main() {\n    let wobble_thing = #(wibble.Wobble, Thing2(23))\n    case wobble_thing {\n        #(wibble.Wibble, Thing1) -> Nil\n    }\n}\n"
+---
+error: Inexhaustive patterns
+  ┌─ /src/one/two.gleam:6:5
+  │  
+6 │ ╭     case wobble_thing {
+7 │ │         #(wibble.Wibble, Thing1) -> Nil
+8 │ │     }
+  │ ╰─────^
+
+This case expression does not have a pattern for all possible values.
+If it is run on one of the values without a pattern then it will crash.
+
+The missing patterns are:
+
+    #(_, Thing2(_))
+    #(wibble.Wobble, Thing1)
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__case_error_prints_module_when_aliased_and_shadowed.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__case_error_prints_module_when_aliased_and_shadowed.snap
new file mode 100644
index 000000000..f86941fb5
--- /dev/null
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__case_error_prints_module_when_aliased_and_shadowed.snap
@@ -0,0 +1,18 @@
+---
+source: compiler-core/src/type_/tests/exhaustiveness.rs
+expression: "\nimport mod.{Wibble as Wobble}\ntype Wibble { Wobble Wubble }\npub fn main() {\n  let wibble = mod.Wibble\n  case wibble {\n    mod.Wobble -> Nil\n  }\n}\n"
+---
+error: Inexhaustive patterns
+  ┌─ /src/one/two.gleam:6:3
+  │  
+6 │ ╭   case wibble {
+7 │ │     mod.Wobble -> Nil
+8 │ │   }
+  │ ╰───^
+
+This case expression does not have a pattern for all possible values.
+If it is run on one of the values without a pattern then it will crash.
+
+The missing patterns are:
+
+    mod.Wibble
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__case_error_prints_module_when_shadowed.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__case_error_prints_module_when_shadowed.snap
new file mode 100644
index 000000000..90a711b42
--- /dev/null
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__case_error_prints_module_when_shadowed.snap
@@ -0,0 +1,18 @@
+---
+source: compiler-core/src/type_/tests/exhaustiveness.rs
+expression: "\nimport mod.{Wibble}\ntype Wibble { Wibble Wobble }\npub fn main() {\n  let wibble = mod.Wibble\n  case wibble {\n    mod.Wobble -> Nil\n  }\n}\n"
+---
+error: Inexhaustive patterns
+  ┌─ /src/one/two.gleam:6:3
+  │  
+6 │ ╭   case wibble {
+7 │ │     mod.Wobble -> Nil
+8 │ │   }
+  │ ╰───^
+
+This case expression does not have a pattern for all possible values.
+If it is run on one of the values without a pattern then it will crash.
+
+The missing patterns are:
+
+    mod.Wibble
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__case_error_prints_prelude_module_unqualified.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__case_error_prints_prelude_module_unqualified.snap
new file mode 100644
index 000000000..83db148e4
--- /dev/null
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__case_error_prints_prelude_module_unqualified.snap
@@ -0,0 +1,18 @@
+---
+source: compiler-core/src/type_/tests/exhaustiveness.rs
+expression: "\npub fn main() {\n  let result = Ok(Nil)\n  case result {\n    Ok(Nil) -> Nil\n  }\n}\n"
+---
+error: Inexhaustive patterns
+  ┌─ /src/one/two.gleam:4:3
+  │  
+4 │ ╭   case result {
+5 │ │     Ok(Nil) -> Nil
+6 │ │   }
+  │ ╰───^
+
+This case expression does not have a pattern for all possible values.
+If it is run on one of the values without a pattern then it will crash.
+
+The missing patterns are:
+
+    Error(_)
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__case_error_prints_prelude_module_when_shadowed.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__case_error_prints_prelude_module_when_shadowed.snap
new file mode 100644
index 000000000..4961637bf
--- /dev/null
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__case_error_prints_prelude_module_when_shadowed.snap
@@ -0,0 +1,18 @@
+---
+source: compiler-core/src/type_/tests/exhaustiveness.rs
+expression: "\nimport gleam\ntype MyResult { Ok Error }\npub fn main() {\n  let res = gleam.Ok(10)\n  case res {\n    gleam.Ok(n) -> Nil\n  }\n}\n"
+---
+error: Inexhaustive patterns
+  ┌─ /src/one/two.gleam:6:3
+  │  
+6 │ ╭   case res {
+7 │ │     gleam.Ok(n) -> Nil
+8 │ │   }
+  │ ╰───^
+
+This case expression does not have a pattern for all possible values.
+If it is run on one of the values without a pattern then it will crash.
+
+The missing patterns are:
+
+    gleam.Error(_)
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__case_error_prints_unqualifed_when_aliased.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__case_error_prints_unqualifed_when_aliased.snap
new file mode 100644
index 000000000..86914ef91
--- /dev/null
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__case_error_prints_unqualifed_when_aliased.snap
@@ -0,0 +1,18 @@
+---
+source: compiler-core/src/type_/tests/exhaustiveness.rs
+expression: "\nimport mod.{Wibble as Wobble}\ntype Wibble { Wibble Wubble }\npub fn main() {\n  let wibble = mod.Wibble\n  case wibble {\n    mod.Wobble -> Nil\n  }\n}\n"
+---
+error: Inexhaustive patterns
+  ┌─ /src/one/two.gleam:6:3
+  │  
+6 │ ╭   case wibble {
+7 │ │     mod.Wobble -> Nil
+8 │ │   }
+  │ ╰───^
+
+This case expression does not have a pattern for all possible values.
+If it is run on one of the values without a pattern then it will crash.
+
+The missing patterns are:
+
+    Wobble
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__case_error_prints_unqualified_value.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__case_error_prints_unqualified_value.snap
new file mode 100644
index 000000000..04eda3ed1
--- /dev/null
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__case_error_prints_unqualified_value.snap
@@ -0,0 +1,18 @@
+---
+source: compiler-core/src/type_/tests/exhaustiveness.rs
+expression: "\nimport wibble.{Wibble, Wobble}\npub fn main() {\n    case Wobble {\n        Wibble -> Nil\n    }\n}\n"
+---
+error: Inexhaustive patterns
+  ┌─ /src/one/two.gleam:4:5
+  │  
+4 │ ╭     case Wobble {
+5 │ │         Wibble -> Nil
+6 │ │     }
+  │ ╰─────^
+
+This case expression does not have a pattern for all possible values.
+If it is run on one of the values without a pattern then it will crash.
+
+The missing patterns are:
+
+    Wobble