Repository: gleam-lang/gleam (rust)

Issue:
Use labels when inserting missing patterns
Right now the "fill in missing patterns" code action adds constructors by putting placeholders `_` for each field, labelled or not. It would be really nice if it also automatically inserted the label name for labelled fields:
```gleam
type Wibble {
  Wibble(Int, Int, label: Int)
  Wobble(label: String, another_one: Int)
}

// Generated code today
case todo {
  Wibble(_, _, _) -> todo
  Wobble(_, _) -> todo
}

// Would be nice if it added the fields' labels
case todo {
  Wibble(_, _, label: _) -> todo
  Wobble(label: _, another_one: _) -> todo
}
```

Pull request description:
Use labels when inserting missing patterns
Closes #4424 


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 13faf02a1..297d21f2a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -30,6 +30,42 @@
 
   ([Giacomo Cavalieri](https://github.com/giacomocavalieri))
 
+- The compiler will now include labels in the error message when a `case`
+  expression is inexhaustive. For example, this code:
+
+  ```gleam
+  pub type Person {
+    Person(name: String, age: Int)
+  }
+
+  pub fn classify(person: Person) {
+    case person {
+      Person(name: "John", age: 27) -> todo
+      Person(name: _, age: 42) -> todo
+    }
+  }
+  ```
+
+  Will produces this error:
+
+  ```
+  error: Inexhaustive patterns
+    ┌─ /src/main.gleam:6:3
+    │
+  6 │ ╭   case person {
+  7 │ │     Person(name: "John", age: 27) -> todo
+  8 │ │     Person(name: _, age: 42) -> todo
+  9 │ │   }
+    │ ╰───^
+
+  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:
+
+      Person(name:, age:)
+  ```
+
 ### Build tool
 
 - The build tool now supports placing modules in a directory called `dev`,
@@ -77,6 +113,38 @@
 
   ([Louis Pilfold](https://github.com/lpil))
 
+- The code action to add missing patterns to a `case` expression now includes
+  labels in the generated patterns. For example:
+
+  ```gleam
+  pub type Person {
+    Person(name: String, age: Int)
+  }
+
+  pub fn classify(person: Person) {
+    case person {
+      Person(name: "John", age: 27) -> todo
+      Person(name: _, age: 42) -> todo
+    }
+  }
+  ```
+
+  Will now become:
+
+  ```gleam
+  pub type Person {
+    Person(name: String, age: Int)
+  }
+
+  pub fn classify(person: Person) {
+    case person {
+      Person(name: "John", age: 27) -> todo
+      Person(name: _, age: 42) -> todo
+      Person(name:, age:) -> todo
+    }
+  }
+  ```
+
 ### Formatter
 
 ### Bug fixes
diff --git a/compiler-core/src/exhaustiveness/missing_patterns.rs b/compiler-core/src/exhaustiveness/missing_patterns.rs
index c201d0bb0..f26192db1 100644
--- a/compiler-core/src/exhaustiveness/missing_patterns.rs
+++ b/compiler-core/src/exhaustiveness/missing_patterns.rs
@@ -19,6 +19,12 @@ pub fn missing_patterns(
     missing
 }
 
+#[derive(Debug, Clone)]
+pub struct VariantField {
+    pub label: Option<EcoString>,
+    pub variable: Variable,
+}
+
 /// Information about a single constructor/value (aka term) being tested, used
 /// to build a list of names of missing patterns.
 #[derive(Debug)]
@@ -27,7 +33,7 @@ pub enum Term {
         variable: Variable,
         name: EcoString,
         module: EcoString,
-        fields: Vec<Variable>,
+        fields: Vec<VariantField>,
     },
     Tuple {
         variable: Variable,
@@ -162,7 +168,12 @@ impl<'a, 'env> MissingPatternsGenerator<'a, 'env> {
                 elements: elements.clone(),
             },
 
-            RuntimeCheck::Variant { index, fields, .. } => {
+            RuntimeCheck::Variant {
+                index,
+                fields,
+                labels,
+                ..
+            } => {
                 let (module, name) = variable
                     .type_
                     .named_type_name()
@@ -178,11 +189,20 @@ impl<'a, 'env> MissingPatternsGenerator<'a, 'env> {
                     .name
                     .clone();
 
+                let fields = fields
+                    .iter()
+                    .enumerate()
+                    .map(|(index, variable)| VariantField {
+                        label: labels.get(&index).cloned(),
+                        variable: variable.clone(),
+                    })
+                    .collect();
+
                 Term::Variant {
                     variable,
                     name,
                     module,
-                    fields: fields.clone(),
+                    fields,
                 }
             }
 
diff --git a/compiler-core/src/exhaustiveness/printer.rs b/compiler-core/src/exhaustiveness/printer.rs
index 13631023c..eea761600 100644
--- a/compiler-core/src/exhaustiveness/printer.rs
+++ b/compiler-core/src/exhaustiveness/printer.rs
@@ -71,19 +71,43 @@ impl<'a> Printer<'a> {
                     return;
                 }
                 buffer.push('(');
-                for (i, variable) in fields.iter().enumerate() {
+                for (i, field) in fields.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 {
+                    let mut has_label = false;
+
+                    if let Some(label) = &field.label {
+                        buffer.push_str(label);
+                        buffer.push(':');
+                        has_label = true;
+                    }
+
+                    if let Some(&idx) = mapping.get(&field.variable.id) {
+                        let term = terms.get(idx).expect("Term must exist");
+
+                        match term {
+                            // If it is an infinite term and this field is labelled, it is generally
+                            // more useful to print just the label using label shorthand syntax.
+                            // For example, printing `Person(name:, age:)` instead of
+                            // `Person(name: _, age: _)`.
+                            Term::Infinite { .. } if has_label => {}
+                            Term::Infinite { .. }
+                            | Term::Variant { .. }
+                            | Term::Tuple { .. }
+                            | Term::EmptyList { .. }
+                            | Term::List { .. } => {
+                                // If this field has a label, the current buffer looks like `label:`,
+                                // so we want to print a space before printing the pattern for it.
+                                // If there is no label, we don't need to print the space.
+                                if has_label {
+                                    buffer.push(' ');
+                                }
+                                self.print(term, terms, mapping, buffer);
+                            }
+                        }
+                    } else if !has_label {
                         buffer.push('_');
                     }
                 }
@@ -163,11 +187,16 @@ impl<'a> Printer<'a> {
 
 #[cfg(test)]
 mod tests {
+    use ecow::EcoString;
+
     use super::Printer;
     use std::{collections::HashMap, sync::Arc};
 
     use crate::{
-        exhaustiveness::{Variable, missing_patterns::Term},
+        exhaustiveness::{
+            Variable,
+            missing_patterns::{Term, VariantField},
+        },
         type_::{Type, printer::Names},
     };
 
@@ -181,6 +210,13 @@ mod tests {
         }
     }
 
+    fn field(variable: Variable, label: Option<&str>) -> VariantField {
+        VariantField {
+            variable,
+            label: label.map(EcoString::from),
+        }
+    }
+
     fn get_mapping(terms: &[Term]) -> HashMap<usize, usize> {
         let mut mapping: HashMap<usize, usize> = HashMap::new();
 
@@ -229,7 +265,7 @@ mod tests {
             variable: subjects[0].clone(),
             name: "Wibble".into(),
             module: "module".into(),
-            fields: vec![var1.clone(), var2.clone()],
+            fields: vec![field(var1.clone(), None), field(var2.clone(), None)],
         };
 
         let terms = &[
@@ -245,6 +281,42 @@ mod tests {
         );
     }
 
+    #[test]
+    fn test_value_in_current_module_with_labelled_arguments() {
+        let mut names = Names::new();
+
+        names.named_constructor_in_scope("module".into(), "Wibble".into(), "Wibble".into());
+
+        let printer = Printer::new(&names);
+
+        let var1 = make_variable(1);
+
+        let var2 = make_variable(2);
+
+        let subjects = &[make_variable(0)];
+        let term = Term::Variant {
+            variable: subjects[0].clone(),
+            name: "Wibble".into(),
+            module: "module".into(),
+            fields: vec![
+                field(var1.clone(), Some("list")),
+                field(var2.clone(), Some("other")),
+            ],
+        };
+
+        let terms = &[
+            term,
+            Term::EmptyList { variable: var1 },
+            Term::Infinite { variable: var2 },
+        ];
+        let mapping = get_mapping(terms);
+
+        assert_eq!(
+            printer.print_terms(subjects, terms, &mapping),
+            "Wibble(list: [], other:)"
+        );
+    }
+
     #[test]
     fn test_module_alias() {
         let mut names = Names::new();
@@ -285,7 +357,7 @@ mod tests {
             variable: subjects[0].clone(),
             name: "Regex".into(),
             module: "regex".into(),
-            fields: vec![arg.clone()],
+            fields: vec![field(arg.clone(), None)],
         };
 
         let terms = &[term, Term::Infinite { variable: arg }];
@@ -310,7 +382,7 @@ mod tests {
             variable: subjects[0].clone(),
             name: "Regex".into(),
             module: "regex".into(),
-            fields: vec![arg.clone()],
+            fields: vec![field(arg.clone(), None)],
         };
 
         let terms = &[
@@ -385,7 +457,7 @@ mod tests {
                 variable: subjects[0].clone(),
                 name: "Ok".into(),
                 module: "gleam".into(),
-                fields: vec![make_variable(3)],
+                fields: vec![field(make_variable(3), None)],
             },
             Term::Variant {
                 variable: subjects[2].clone(),


Test patch:
diff --git a/compiler-core/src/language_server/tests/action.rs b/compiler-core/src/language_server/tests/action.rs
index d3feaf880..728f8c79f 100644
--- a/compiler-core/src/language_server/tests/action.rs
+++ b/compiler-core/src/language_server/tests/action.rs
@@ -7719,3 +7719,21 @@ pub fn main() {
         find_position_of("Wobble()").to_selection(),
     );
 }
+
+#[test]
+fn add_missing_patterns_with_labels() {
+    assert_code_action!(
+        ADD_MISSING_PATTERNS,
+        "
+pub type Wibble {
+  Wibble(integer: Int, float: Float)
+  Wobble(string: String, bool: Bool)
+}
+
+pub fn main(w: Wibble) {
+  case w {}
+}
+",
+        find_position_of("case w").select_until(find_position_of("{}")),
+    );
+}
diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__add_missing_patterns_with_labels.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__add_missing_patterns_with_labels.snap
new file mode 100644
index 000000000..07b32725c
--- /dev/null
+++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__add_missing_patterns_with_labels.snap
@@ -0,0 +1,30 @@
+---
+source: compiler-core/src/language_server/tests/action.rs
+expression: "\npub type Wibble {\n  Wibble(integer: Int, float: Float)\n  Wobble(string: String, bool: Bool)\n}\n\npub fn main(w: Wibble) {\n  case w {}\n}\n"
+---
+----- BEFORE ACTION
+
+pub type Wibble {
+  Wibble(integer: Int, float: Float)
+  Wobble(string: String, bool: Bool)
+}
+
+pub fn main(w: Wibble) {
+  case w {}
+  ▔▔▔▔▔▔▔↑ 
+}
+
+
+----- AFTER ACTION
+
+pub type Wibble {
+  Wibble(integer: Int, float: Float)
+  Wobble(string: String, bool: Bool)
+}
+
+pub fn main(w: Wibble) {
+  case w {
+    Wibble(integer:, float:) -> todo
+    Wobble(string:, bool:) -> todo
+  }
+}
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__discard_2.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__discard_2.snap
index e1974c5cd..332bd17e0 100644
--- a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__discard_2.snap
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__discard_2.snap
@@ -29,4 +29,4 @@ is run on one of the values without a pattern then it will crash.
 
 The missing patterns are:
 
-    Thing(False, _)
+    Thing(a: False, b:)
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__discard_3.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__discard_3.snap
index 8f107140e..1d78dc36a 100644
--- a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__discard_3.snap
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__discard_3.snap
@@ -29,4 +29,4 @@ is run on one of the values without a pattern then it will crash.
 
 The missing patterns are:
 
-    Thing(True, _)
+    Thing(a: True, b:)
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__discard_4.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__discard_4.snap
index e1974c5cd..332bd17e0 100644
--- a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__discard_4.snap
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__discard_4.snap
@@ -29,4 +29,4 @@ is run on one of the values without a pattern then it will crash.
 
 The missing patterns are:
 
-    Thing(False, _)
+    Thing(a: False, b:)
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__discard_5.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__discard_5.snap
index 8f107140e..1d78dc36a 100644
--- a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__discard_5.snap
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__discard_5.snap
@@ -29,4 +29,4 @@ is run on one of the values without a pattern then it will crash.
 
 The missing patterns are:
 
-    Thing(True, _)
+    Thing(a: True, b:)
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__discard_6.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__discard_6.snap
index 74d89931f..a7550bb25 100644
--- a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__discard_6.snap
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__discard_6.snap
@@ -29,4 +29,4 @@ is run on one of the values without a pattern then it will crash.
 
 The missing patterns are:
 
-    Thing(True, _)
+    Thing(a: True, b:)
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__label_1.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__label_1.snap
index a6a98c933..4b0b61c66 100644
--- a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__label_1.snap
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__exhaustiveness__label_1.snap
@@ -31,5 +31,5 @@ is run on one of the values without a pattern then it will crash.
 
 The missing patterns are:
 
-    Thing(False, False)
-    Thing(True, True)
+    Thing(a: False, b: False)
+    Thing(a: True, b: True)