Repository: gleam-lang/gleam (rust)

Issue:
Deprecate `..` discard if not preceded by a comma
I found a piece of syntax we should deprecate like we did with the list discard
```gleam
Wibble(one: name ..)
// Is valid and interpreted as
Wibble(one: name, ..)

// and
Wibble(one: ..)
// Is valid and interpreted as
Wibble(one:, ..)
```

Pull request:
Deprecate record pattern matching syntax
This PR closes #3473

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 364d86e4f..2b51857bc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -59,6 +59,10 @@
   Now inferes correctly to return `Int`.
   ([sobolevn](https://github.com/sobolevn))
 
+- The record pattern matching syntax `Record(a ..)` is now deprecated in favour
+  of the `Record(a, ..)` syntax.
+  ([Giacomo Cavalieri](https://github.com/giacomocavalieri))
+
 ### Formatter
 
 ### Language Server
diff --git a/compiler-core/src/parse.rs b/compiler-core/src/parse.rs
index f73fd87f2..39aaea4b1 100644
--- a/compiler-core/src/parse.rs
+++ b/compiler-core/src/parse.rs
@@ -1629,17 +1629,23 @@ where
         upname_end: u32,
     ) -> Result<(Vec<CallArg<UntypedPattern>>, Option<SrcSpan>, u32), ParseError> {
         if self.maybe_one(&Token::LeftParen).is_some() {
-            let args = Parser::series_of(
-                self,
+            let (args, args_end_with_comma) = self.series_of_has_trailing_separator(
                 &Parser::parse_constructor_pattern_arg,
                 Some(&Token::Comma),
             )?;
+
             let spread = self
                 .maybe_one(&Token::DotDot)
                 .map(|(start, end)| SrcSpan { start, end });
 
-            if spread.is_some() {
+            if let Some(spread_location) = spread {
                 let _ = self.maybe_one(&Token::Comma);
+                if !args.is_empty() && !args_end_with_comma {
+                    self.warnings
+                        .push(DeprecatedSyntaxWarning::DeprecatedRecordSpreadPattern {
+                            location: spread_location,
+                        })
+                }
             }
             let (_, end) = self.expect_one(&Token::RightParen)?;
             Ok((args, spread, end))
diff --git a/compiler-core/src/warning.rs b/compiler-core/src/warning.rs
index ffd7eca92..c3397e22a 100644
--- a/compiler-core/src/warning.rs
+++ b/compiler-core/src/warning.rs
@@ -188,6 +188,16 @@ pub enum DeprecatedSyntaxWarning {
     /// ```
     ///
     DeprecatedListCatchAllPattern { location: SrcSpan },
+
+    /// If a record pattern has a spread that is not preceded by a comma:
+    /// ```gleam
+    /// case wibble {
+    ///   Wibble(arg1: name ..) -> todo
+    /// //                  ^^ this should be preceded by a comma!
+    /// }
+    /// ```
+    ///
+    DeprecatedRecordSpreadPattern { location: SrcSpan },
 }
 
 impl Warning {
@@ -255,6 +265,26 @@ like this: `[item, ..list]`.",
                 }),
             },
 
+            Warning::DeprecatedSyntax {
+                path,
+                src,
+                warning: DeprecatedSyntaxWarning::DeprecatedRecordSpreadPattern { location },
+            } => Diagnostic {
+                title: "Deprecated record pattern matching syntax".into(),
+                text: wrap("This syntax for pattern matching on a record is deprecated."),
+                hint: None,
+                level: diagnostic::Level::Warning,
+                location: Some(Location {
+                    label: diagnostic::Label {
+                        text: Some("This should be preceded by a comma".into()),
+                        span: *location,
+                    },
+                    path: path.clone(),
+                    src: src.clone(),
+                    extra_labels: vec![],
+                }),
+            },
+
             Warning::DeprecatedSyntax {
                 path,
                 src,


Test patch:
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__deprecated_record_pattern_syntax.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__deprecated_record_pattern_syntax.snap
new file mode 100644
index 000000000..078113ed6
--- /dev/null
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__deprecated_record_pattern_syntax.snap
@@ -0,0 +1,11 @@
+---
+source: compiler-core/src/type_/tests/warnings.rs
+expression: "\npub type Wibble {\n  Wibble(one: Int, two: Int)\n}\n\npub fn main() {\n  let wibble = Wibble(one: 1, two: 2)\n  case wibble {\n    Wibble(one: one ..) -> one\n  }\n}\n"
+---
+warning: Deprecated record pattern matching syntax
+  ┌─ test/path:9:21
+  │
+9 │     Wibble(one: one ..) -> one
+  │                     ^^ This should be preceded by a comma
+
+This syntax for pattern matching on a record is deprecated.
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__deprecated_record_pattern_syntax_with_label_shorthand.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__deprecated_record_pattern_syntax_with_label_shorthand.snap
new file mode 100644
index 000000000..74e231385
--- /dev/null
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__deprecated_record_pattern_syntax_with_label_shorthand.snap
@@ -0,0 +1,11 @@
+---
+source: compiler-core/src/type_/tests/warnings.rs
+expression: "\npub type Wibble {\n  Wibble(one: Int, two: Int)\n}\n\npub fn main() {\n  let wibble = Wibble(one: 1, two: 2)\n  case wibble {\n    Wibble(one: ..) -> one\n  }\n}\n"
+---
+warning: Deprecated record pattern matching syntax
+  ┌─ test/path:9:17
+  │
+9 │     Wibble(one: ..) -> one
+  │                 ^^ This should be preceded by a comma
+
+This syntax for pattern matching on a record is deprecated.
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__deprecated_record_pattern_syntax_with_no_labels.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__deprecated_record_pattern_syntax_with_no_labels.snap
new file mode 100644
index 000000000..7d6486729
--- /dev/null
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__deprecated_record_pattern_syntax_with_no_labels.snap
@@ -0,0 +1,11 @@
+---
+source: compiler-core/src/type_/tests/warnings.rs
+expression: "\npub type Wibble {\n  Wibble(one: Int, two: Int)\n}\n\npub fn main() {\n  let wibble = Wibble(one: 1, two: 2)\n  case wibble {\n    Wibble(one ..) -> one\n  }\n}\n"
+---
+warning: Deprecated record pattern matching syntax
+  ┌─ test/path:9:16
+  │
+9 │     Wibble(one ..) -> one
+  │                ^^ This should be preceded by a comma
+
+This syntax for pattern matching on a record is deprecated.
diff --git a/compiler-core/src/type_/tests/warnings.rs b/compiler-core/src/type_/tests/warnings.rs
index 25388a8ab..13b17bbf6 100644
--- a/compiler-core/src/type_/tests/warnings.rs
+++ b/compiler-core/src/type_/tests/warnings.rs
@@ -1986,6 +1986,97 @@ fn deprecated_list_pattern_syntax_1() {
     );
 }
 
+// https://github.com/gleam-lang/gleam/issues/3473
+#[test]
+fn deprecated_record_pattern_syntax() {
+    assert_warning!(
+        r#"
+pub type Wibble {
+  Wibble(one: Int, two: Int)
+}
+
+pub fn main() {
+  let wibble = Wibble(one: 1, two: 2)
+  case wibble {
+    Wibble(one: one ..) -> one
+  }
+}
+"#
+    );
+}
+
+#[test]
+fn deprecated_record_pattern_syntax_with_no_labels() {
+    assert_warning!(
+        r#"
+pub type Wibble {
+  Wibble(one: Int, two: Int)
+}
+
+pub fn main() {
+  let wibble = Wibble(one: 1, two: 2)
+  case wibble {
+    Wibble(one ..) -> one
+  }
+}
+"#
+    );
+}
+
+#[test]
+fn deprecated_record_pattern_syntax_with_label_shorthand() {
+    assert_warning!(
+        r#"
+pub type Wibble {
+  Wibble(one: Int, two: Int)
+}
+
+pub fn main() {
+  let wibble = Wibble(one: 1, two: 2)
+  case wibble {
+    Wibble(one: ..) -> one
+  }
+}
+"#
+    );
+}
+
+#[test]
+fn deprecated_record_pattern_syntax_has_no_warning_if_everything_is_discarded() {
+    assert_no_warnings!(
+        r#"
+pub type Wibble {
+  Wibble(one: Int, two: Int)
+}
+
+pub fn main() {
+  let wibble = Wibble(one: 1, two: 2)
+  case wibble {
+    Wibble(..) -> 1
+  }
+}
+"#
+    );
+}
+
+#[test]
+fn deprecated_record_pattern_syntax_has_no_warning_if_there_is_a_comma_before_spread() {
+    assert_no_warnings!(
+        r#"
+pub type Wibble {
+  Wibble(one: Int, two: Int)
+}
+
+pub fn main() {
+  let wibble = Wibble(one: 1, two: 2)
+  case wibble {
+    Wibble(one: one, ..) -> one
+  }
+}
+"#
+    );
+}
+
 #[test]
 fn unused_label_shorthand_pattern_arg() {
     assert_warning!(