Repository: gleam-lang/gleam (Rust)

Issue:
"Fill labels" produces invalid code on function calls with arguments that don't use their labels
Using the "Fill labels" code action on a function call that has labeled arguments being called without the label will incorrectly add the label at the end, breaking the arity in some cases.

```gleam
list.fold(0, over: [1, 2, 3], with: fn(total, item) { total + item })
```
turns into 
```gleam
list.fold(0, over: [1, 2, 3], with: fn(total, item) { total + item }, from: todo)
```
which is invalid and, promptly, raises an error.

The fix should be simple, because this doesn't happen for filling labels in records.

One option would be to just not offer the code action in this case (like it behaves for records). However, it would be extra nice if it filled in the label in the correct position, but, alas, it would also be extra work...


Discussion:
Thank you
From some tests it looks like it only happens when labels have a certain order, I'm looking into this!



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bf0956a0ec3..632136efc9e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -339,3 +339,7 @@
   patterns in a `case` expression when the clause matched on multiple
   alternative patterns.
   ([Surya Rose](https://github.com/GearsDatapacks))
+
+- Fixed a bug where the language server would generate invalid code for the
+  "fill in missing labels" code action.
+  ([Giacomo Cavalieri](https://github.com/giacomocavalieri))
diff --git a/compiler-core/src/type_/fields.rs b/compiler-core/src/type_/fields.rs
index d657c9c1508..6939c3e3011 100644
--- a/compiler-core/src/type_/fields.rs
+++ b/compiler-core/src/type_/fields.rs
@@ -161,44 +161,39 @@ impl FieldMap {
     /// ```
     ///
     pub fn missing_labels<A: std::fmt::Debug>(&self, args: &[CallArg<A>]) -> Vec<EcoString> {
-        let mut arg_position_to_label = self
-            .fields
-            .iter()
-            .map(|(label, position)| (position, label.clone()))
-            .collect::<HashMap<_, _>>();
-
-        // We first get rid of all the labels taken by the positional arguments
-        // that have been supplied.
-        let mut position = 0;
-        for arg in args {
-            if arg.label.is_none() && !arg.is_use_implicit_callback() {
-                let _ = arg_position_to_label.remove(&position);
-                position += 1;
-            } else {
-                // As soon as we find an unlabelled argument we break out of the
-                // loop, we know that now there's only going to be labelled
-                // arguments
-                break;
-            }
-        }
+        // We need to know how many positional arguments are in the function
+        // arguments. That's given by the position of the first labelled
+        // argument; if the first label argument is third, then we know the
+        // function also needs two unlabelled arguments first.
+        let Some(positional_arguments) = self.fields.values().min().cloned() else {
+            return vec![];
+        };
 
-        let mut arg_label_to_position = arg_position_to_label
+        // We need to count how many positional arguments were actually supplied
+        // in the call, to remove the corresponding labelled arguments that have
+        // been taken by any positional argument.
+        let given_positional_arguments = args
             .iter()
-            .map(|(position, label)| (label.clone(), position))
-            .collect::<HashMap<_, _>>();
+            .filter(|argument| argument.label.is_none() && !argument.is_use_implicit_callback())
+            .count();
 
-        // Now we're just left with labelled args, we remove those from the
-        // remaining labels.
-        for arg in args.iter().skip(position as usize) {
-            if let Some(label) = &arg.label {
-                let _ = arg_label_to_position.remove(label);
-            }
-        }
+        let explicit_labels = args
+            .iter()
+            .filter_map(|argument| argument.label.as_ref())
+            .collect::<HashSet<&EcoString>>();
 
-        arg_label_to_position
+        self.fields
             .iter()
+            // As a start we remove all the labels that are already used explicitly,
+            // for sure those are not going to be unused!
+            .filter(|(label, _)| !explicit_labels.contains(label))
+            // ...then we sort all the labels in order by their original position in
+            // the function definition
             .sorted_by_key(|(_, position)| *position)
-            .map(|(label, _position)| label.clone())
+            // ... finally we remove all the ones that are taken by a positional
+            // argument
+            .dropping(given_positional_arguments.saturating_sub(positional_arguments as usize))
+            .map(|(label, _)| label.clone())
             .collect_vec()
     }
 


Test patch:
diff --git a/compiler-core/src/language_server/tests/action.rs b/compiler-core/src/language_server/tests/action.rs
index a53d0df4b73..99400e4d33e 100644
--- a/compiler-core/src/language_server/tests/action.rs
+++ b/compiler-core/src/language_server/tests/action.rs
@@ -7797,6 +7797,21 @@ pub fn main() {
     );
 }
 
+#[test]
+// https://github.com/gleam-lang/gleam/issues/4499
+fn fill_labels_with_function_with_unlabelled_arguments() {
+    assert_no_code_actions!(
+        FILL_LABELS,
+        "
+pub fn main() {
+    fold(0, over: [], with: fn(acc, item) { acc + item })
+}
+
+pub fn fold(over list, from initial, with fun) { todo }",
+        find_position_of("fold").to_selection(),
+    );
+}
+
 #[test]
 fn add_missing_patterns_with_labels() {
     assert_code_action!(