Repository: gleam-lang/gleam (rust)

Issue:
Add fault tolerant parsing for `case` expressions
Currently, `case something` is a syntax error (obviously). The problem is, the language server cannot provide autocomplete when typing the subject of the case expression. We could make this fault tolerant in the same way as `record.`, to allow for a better LSP experience.

Pull request:
Make case expressions fault tolerant
Closes #3978
This PR makes parsing of case expressions fault tolerant, allowing analysis to continue when a case expressions is missing a body. This mostly allows for LSP completions for the case subjects.

Patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 866683e8c..f0ffbd941 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -41,39 +41,39 @@ jobs:
           - os: ubuntu-latest
             target: x86_64-unknown-linux-gnu
             binary: x86-64
-            use-cross: false
+            cargo-tool: cargo
             run-integration-tests: true
           - os: ubuntu-latest
             target: x86_64-unknown-linux-musl
             binary: x86-64
-            use-cross: true
+            cargo-tool: cross
             run-integration-tests: true
           - os: ubuntu-latest
             target: aarch64-unknown-linux-gnu
             binary: aarch64
-            use-cross: true
+            cargo-tool: cross
             run-integration-tests: false # Cannot run aarch64 binaries on x86_64
           - os: ubuntu-latest
             target: aarch64-unknown-linux-musl
             binary: aarch64
-            use-cross: true
+            cargo-tool: cross
             run-integration-tests: false # Cannot run aarch64 binaries on x86_64
           # macos>=14 runs exclusively on aarch64 and will thus fail to execute properly for x64
           - os: macos-13 # intel
             target: x86_64-apple-darwin
             binary: x86_64
-            use-cross: false
+            cargo-tool: cargo
             run-integration-tests: true
           - os: macos-latest # aarch64
             toolchain: stable
             target: aarch64-apple-darwin
             binary: arm64
-            use-cross: false
+            cargo-tool: cargo
             run-integration-tests: true
           - os: windows-latest
             target: x86_64-pc-windows-msvc
             binary: x86-64
-            use-cross: false
+            cargo-tool: cargo
             run-integration-tests: true
     steps:
       - name: Checkout repository
@@ -113,11 +113,11 @@ jobs:
           key: v1-${{ matrix.target }}
 
       - name: Install Gleam
-        uses: clechasseur/rs-cargo@v2
+        uses: clechasseur/rs-cargo@v3
         with:
           command: install
           args: "--path compiler-cli --target ${{ matrix.target }} --debug --locked --force"
-          use-cross: ${{ matrix.use-cross }}
+          tool: ${{ matrix.cargo-tool }}
         if: ${{ matrix.run-integration-tests }}
 
       - name: Verify binary architecture
@@ -138,11 +138,11 @@ jobs:
         if: ${{ matrix.run-integration-tests }}
 
       - name: Run tests
-        uses: clechasseur/rs-cargo@v2
+        uses: clechasseur/rs-cargo@v3
         with:
           command: test
           args: "--workspace --target ${{ matrix.target }}"
-          use-cross: ${{ matrix.use-cross }}
+          tool: ${{ matrix.cargo-tool }}
 
       - name: test/project_erlang (non-windows)
         run: |
diff --git a/.github/workflows/release-nightly.yaml b/.github/workflows/release-nightly.yaml
index d6cbca91e..e635f0535 100644
--- a/.github/workflows/release-nightly.yaml
+++ b/.github/workflows/release-nightly.yaml
@@ -63,24 +63,24 @@ jobs:
           - os: ubuntu-latest
             target: x86_64-unknown-linux-musl
             binary: x86-64
-            use-cross: true
+            cargo-tool: cross
           - os: ubuntu-latest
             target: aarch64-unknown-linux-musl
             binary: aarch64
-            use-cross: true
+            cargo-tool: cross
           # macos>=14 runs exclusively on aarch64 and will thus fail to execute properly for x64
           - os: macos-13
             target: x86_64-apple-darwin
             binary: x86_64
-            use-cross: false
+            cargo-tool: cargo
           - os: macos-latest
             target: aarch64-apple-darwin
             binary: arm64
-            use-cross: false
+            cargo-tool: cargo
           - os: windows-latest
             target: x86_64-pc-windows-msvc
             binary: x86-64
-            use-cross: false
+            cargo-tool: cargo
     steps:
       - name: Checkout repository
         uses: actions/checkout@v4
@@ -102,11 +102,11 @@ jobs:
           key: v1-${{ matrix.target }}
 
       - name: Build release binary
-        uses: clechasseur/rs-cargo@v2
+        uses: clechasseur/rs-cargo@v3
         with:
           command: build
           args: --release --target ${{ matrix.target }}
-          use-cross: ${{ matrix.use-cross }}
+          cargo-tool: ${{ matrix.cargo-tool }}
 
       - name: Verify binary architecture
         shell: bash
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index f050f8eb8..2b0ad2166 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -25,24 +25,24 @@ jobs:
           - os: ubuntu-latest
             target: x86_64-unknown-linux-musl
             binary: x86-64
-            use-cross: true
+            cargo-tool: cross
           - os: ubuntu-latest
             target: aarch64-unknown-linux-musl
             binary: aarch64
-            use-cross: true
+            cargo-tool: cross
           # macos>=14 runs exclusively on aarch64 and will thus fail to execute properly for x64
           - os: macos-13
             target: x86_64-apple-darwin
             binary: x86_64
-            use-cross: false
+            cargo-tool: cargo
           - os: macos-latest
             target: aarch64-apple-darwin
             binary: arm64
-            use-cross: false
+            cargo-tool: cargo
           - os: windows-latest
             target: x86_64-pc-windows-msvc
             binary: x86-64
-            use-cross: false
+            cargo-tool: cargo
     steps:
       - name: Checkout repository
         uses: actions/checkout@v4
@@ -59,11 +59,11 @@ jobs:
           key: v1-${{ matrix.target }}
 
       - name: Build release binary
-        uses: clechasseur/rs-cargo@v2
+        uses: clechasseur/rs-cargo@v3
         with:
           command: build
           args: --release --target ${{ matrix.target }}
-          use-cross: ${{ matrix.use-cross }}
+          tool: ${{ matrix.cargo-tool }}
 
       - name: Verify binary architecture
         shell: bash
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b032f929a..e88589023 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,11 @@
   shell starting from OTP27.
   ([Giacomo Cavalieri](https://github.com/giacomocavalieri))
 
+- Parsing of `case` expressions is now fault tolerant. If a `case` expressions
+  is missing its body, the compiler can still perform type inference. This also
+  allows the Language Server to provide completion hints for `case` subjects.
+  ([Surya Rose](https://github.com/GearsDatapacks))
+
 ### Build tool
 
 - `gleam new` now has refined project name validation - rather than failing on
@@ -125,3 +130,7 @@
 - Fixed a bug where a block expression containing a singular record update would
   produce invalid erlang.
   ([yoshi](https://github.com/joshi-monster))
+
+- Fixed a typo in the error message when trying to import a test module into an
+  application module.
+  ([John Strunk](https://github.com/jrstrunk))
diff --git a/compiler-core/src/ast/untyped.rs b/compiler-core/src/ast/untyped.rs
index 5a1adafcf..112b40be3 100644
--- a/compiler-core/src/ast/untyped.rs
+++ b/compiler-core/src/ast/untyped.rs
@@ -69,7 +69,8 @@ pub enum UntypedExpr {
     Case {
         location: SrcSpan,
         subjects: Vec<Self>,
-        clauses: Vec<Clause<Self, (), ()>>,
+        // None if the case expression is missing a body.
+        clauses: Option<Vec<Clause<Self, (), ()>>>,
     },
 
     FieldAccess {
diff --git a/compiler-core/src/ast_folder.rs b/compiler-core/src/ast_folder.rs
index 8d0c3df23..9c20312af 100644
--- a/compiler-core/src/ast_folder.rs
+++ b/compiler-core/src/ast_folder.rs
@@ -457,23 +457,25 @@ pub trait UntypedExprFolder: TypeAstFolder + UntypedConstantFolder + PatternFold
                 clauses,
             } => {
                 let subjects = subjects.into_iter().map(|e| self.fold_expr(e)).collect();
-                let clauses = clauses
-                    .into_iter()
-                    .map(|mut c| {
-                        c.pattern = c
-                            .pattern
-                            .into_iter()
-                            .map(|p| self.fold_pattern(p))
-                            .collect();
-                        c.alternative_patterns = c
-                            .alternative_patterns
-                            .into_iter()
-                            .map(|p| p.into_iter().map(|p| self.fold_pattern(p)).collect())
-                            .collect();
-                        c.then = self.fold_expr(c.then);
-                        c
-                    })
-                    .collect();
+                let clauses = clauses.map(|clauses| {
+                    clauses
+                        .into_iter()
+                        .map(|mut c| {
+                            c.pattern = c
+                                .pattern
+                                .into_iter()
+                                .map(|p| self.fold_pattern(p))
+                                .collect();
+                            c.alternative_patterns = c
+                                .alternative_patterns
+                                .into_iter()
+                                .map(|p| p.into_iter().map(|p| self.fold_pattern(p)).collect())
+                                .collect();
+                            c.then = self.fold_expr(c.then);
+                            c
+                        })
+                        .collect()
+                });
                 UntypedExpr::Case {
                     location,
                     subjects,
@@ -742,7 +744,7 @@ pub trait UntypedExprFolder: TypeAstFolder + UntypedConstantFolder + PatternFold
         &mut self,
         location: SrcSpan,
         subjects: Vec<UntypedExpr>,
-        clauses: Vec<UntypedClause>,
+        clauses: Option<Vec<UntypedClause>>,
     ) -> UntypedExpr {
         UntypedExpr::Case {
             location,
diff --git a/compiler-core/src/call_graph.rs b/compiler-core/src/call_graph.rs
index c860d4fdb..535ca7544 100644
--- a/compiler-core/src/call_graph.rs
+++ b/compiler-core/src/call_graph.rs
@@ -278,7 +278,7 @@ impl<'a> CallGraphBuilder<'a> {
                 for subject in subjects {
                     self.expression(subject);
                 }
-                for clause in clauses {
+                for clause in clauses.as_deref().unwrap_or_default() {
                     let names = self.names.clone();
                     for pattern in &clause.pattern {
                         self.pattern(pattern);
diff --git a/compiler-core/src/error.rs b/compiler-core/src/error.rs
index 5e5a52a14..80184a62d 100644
--- a/compiler-core/src/error.rs
+++ b/compiler-core/src/error.rs
@@ -1492,7 +1492,7 @@ Erlang's floating point type. To avoid this error float values must be in the ra
                         "The application module `{src_module}` \
 is importing the test module `{test_module}`.
 
-Test modules are not included in production builds so test \
+Test modules are not included in production builds so application \
 modules cannot import them. Perhaps move the `{test_module}` \
 module to the src directory.",
                         );
@@ -3066,6 +3066,27 @@ The missing patterns are:\n"
                     }
                 }
 
+                TypeError::MissingCaseBody { location } => {
+                    let text = wrap(
+                        "This case expression is missing its body."
+                        );
+                    Diagnostic {
+                        title: "Missing case body".into(),
+                        text,
+                        hint: None,
+                        level: Level::Error,
+                        location: Some(Location {
+                            src: src.clone(),
+                            path: path.to_path_buf(),
+                            label: Label {
+                                text: None,
+                                span: *location,
+                            },
+                            extra_labels: Vec::new(),
+                        }),
+                    }
+                }
+
                 TypeError::UnsupportedExpressionTarget {
                     location,
                     target: current_target,
diff --git a/compiler-core/src/format.rs b/compiler-core/src/format.rs
index 2f48ccc9e..8bb47bde7 100644
--- a/compiler-core/src/format.rs
+++ b/compiler-core/src/format.rs
@@ -1030,7 +1030,7 @@ impl<'comments> Formatter<'comments> {
                 subjects,
                 clauses,
                 location,
-            } => self.case(subjects, clauses, location),
+            } => self.case(subjects, clauses.as_deref().unwrap_or_default(), location),
 
             UntypedExpr::FieldAccess {
                 label, container, ..
diff --git a/compiler-core/src/parse.rs b/compiler-core/src/parse.rs
index e59dfd55c..211fbe368 100644
--- a/compiler-core/src/parse.rs
+++ b/compiler-core/src/parse.rs
@@ -701,20 +701,33 @@ where
                 self.advance();
                 let subjects =
                     Parser::series_of(self, &Parser::parse_expression, Some(&Token::Comma))?;
-                let _ = self.expect_one_following_series(&Token::LeftBrace, "an expression")?;
-                let clauses = Parser::series_of(self, &Parser::parse_case_clause, None)?;
-                let (_, end) =
-                    self.expect_one_following_series(&Token::RightBrace, "a case clause")?;
-                if subjects.is_empty() {
-                    return parse_error(
-                        ParseErrorType::ExpectedExpr,
-                        SrcSpan { start, end: case_e },
-                    );
+                if self.maybe_one(&Token::LeftBrace).is_some() {
+                    let clauses = Parser::series_of(self, &Parser::parse_case_clause, None)?;
+                    let (_, end) =
+                        self.expect_one_following_series(&Token::RightBrace, "a case clause")?;
+                    if subjects.is_empty() {
+                        return parse_error(
+                            ParseErrorType::ExpectedExpr,
+                            SrcSpan { start, end: case_e },
+                        );
+                    } else {
+                        UntypedExpr::Case {
+                            location: SrcSpan { start, end },
+                            subjects,
+                            clauses: Some(clauses),
+                        }
+                    }
                 } else {
                     UntypedExpr::Case {
-                        location: SrcSpan { start, end },
+                        location: SrcSpan::new(
+                            start,
+                            subjects
+                                .last()
+                                .map(|subject| subject.location().end)
+                                .unwrap_or(case_e),
+                        ),
                         subjects,
-                        clauses,
+                        clauses: None,
                     }
                 }
             }
diff --git a/compiler-core/src/type_/error.rs b/compiler-core/src/type_/error.rs
index 95ea0118e..7ec6cff49 100644
--- a/compiler-core/src/type_/error.rs
+++ b/compiler-core/src/type_/error.rs
@@ -443,6 +443,11 @@ pub enum Error {
         missing: Vec<EcoString>,
     },
 
+    /// A case expression is missing its body.
+    MissingCaseBody {
+        location: SrcSpan,
+    },
+
     /// Let assignment's pattern does not match all possible values of the type.
     InexhaustiveLetAssignment {
         location: SrcSpan,
@@ -1031,6 +1036,7 @@ impl Error {
             | Error::InvalidExternalJavascriptModule { location, .. }
             | Error::InvalidExternalJavascriptFunction { location, .. }
             | Error::InexhaustiveCaseExpression { location, .. }
+            | Error::MissingCaseBody { location }
             | Error::InexhaustiveLetAssignment { location, .. }
             | Error::UnusedTypeAliasParameter { location, .. }
             | Error::DuplicateTypeParameter { location, .. }
diff --git a/compiler-core/src/type_/expression.rs b/compiler-core/src/type_/expression.rs
index 6a087863c..60a85b801 100644
--- a/compiler-core/src/type_/expression.rs
+++ b/compiler-core/src/type_/expression.rs
@@ -1428,13 +1428,12 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
     fn infer_case(
         &mut self,
         subjects: Vec<UntypedExpr>,
-        clauses: Vec<UntypedClause>,
+        clauses: Option<Vec<UntypedClause>>,
         location: SrcSpan,
     ) -> TypedExpr {
         let subjects_count = subjects.len();
         let mut typed_subjects = Vec::with_capacity(subjects_count);
         let mut subject_types = Vec::with_capacity(subjects_count);
-        let mut typed_clauses = Vec::with_capacity(clauses.len());
 
         let return_type = self.new_unbound_var();
 
@@ -1459,6 +1458,20 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
             typed_subjects.push(subject);
         }
 
+        let clauses = match clauses {
+            Some(clauses) => clauses,
+            None => {
+                self.problems.error(Error::MissingCaseBody { location });
+                return TypedExpr::Case {
+                    location,
+                    type_: return_type,
+                    subjects: typed_subjects,
+                    clauses: Vec::new(),
+                };
+            }
+        };
+
+        let mut typed_clauses = Vec::with_capacity(clauses.len());
         let mut has_a_guard = false;
         let mut all_patterns_are_discards = true;
         // NOTE: if there are 0 clauses then there are 0 panics


Test patch:
diff --git a/compiler-core/src/language_server/tests/completion.rs b/compiler-core/src/language_server/tests/completion.rs
index 4cbd08ae3..91aefeb0d 100644
--- a/compiler-core/src/language_server/tests/completion.rs
+++ b/compiler-core/src/language_server/tests/completion.rs
@@ -1886,3 +1886,18 @@ pub fn map(_, _) { [] }
         Position::new(3, 8)
     );
 }
+
+#[test]
+fn case_subject() {
+    let code = "
+pub fn main(something: Bool) {
+  case so
+}
+";
+
+    assert_apply_completion!(
+        TestProject::for_source(code),
+        "something",
+        Position::new(2, 9)
+    );
+}
diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__completion__case_subject.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__completion__case_subject.snap
new file mode 100644
index 000000000..5f28e0088
--- /dev/null
+++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__completion__case_subject.snap
@@ -0,0 +1,14 @@
+---
+source: compiler-core/src/language_server/tests/completion.rs
+expression: "\npub fn main(something: Bool) {\n  case so\n}\n"
+---
+pub fn main(something: Bool) {
+  case so|
+}
+
+
+----- After applying completion -----
+
+pub fn main(something: Bool) {
+  case something
+}
diff --git a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__arithmetic_in_guards.snap b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__arithmetic_in_guards.snap
index 700217248..432fcc34a 100644
--- a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__arithmetic_in_guards.snap
+++ b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__arithmetic_in_guards.snap
@@ -27,86 +27,88 @@ expression: "\ncase 2, 3 {\n    x, y if x + y == 1 -> True\n}"
                     int_value: 3,
                 },
             ],
-            clauses: [
-                Clause {
-                    location: SrcSpan {
-                        start: 17,
-                        end: 43,
-                    },
-                    pattern: [
-                        Variable {
-                            location: SrcSpan {
-                                start: 17,
-                                end: 18,
-                            },
-                            name: "x",
-                            type_: (),
-                            origin: Variable(
-                                "x",
-                            ),
+            clauses: Some(
+                [
+                    Clause {
+                        location: SrcSpan {
+                            start: 17,
+                            end: 43,
                         },
-                        Variable {
-                            location: SrcSpan {
-                                start: 20,
-                                end: 21,
+                        pattern: [
+                            Variable {
+                                location: SrcSpan {
+                                    start: 17,
+                                    end: 18,
+                                },
+                                name: "x",
+                                type_: (),
+                                origin: Variable(
+                                    "x",
+                                ),
                             },
-                            name: "y",
-                            type_: (),
-                            origin: Variable(
-                                "y",
-                            ),
-                        },
-                    ],
-                    alternative_patterns: [],
-                    guard: Some(
-                        Equals {
-                            location: SrcSpan {
-                                start: 25,
-                                end: 35,
+                            Variable {
+                                location: SrcSpan {
+                                    start: 20,
+                                    end: 21,
+                                },
+                                name: "y",
+                                type_: (),
+                                origin: Variable(
+                                    "y",
+                                ),
                             },
-                            left: AddInt {
+                        ],
+                        alternative_patterns: [],
+                        guard: Some(
+                            Equals {
                                 location: SrcSpan {
                                     start: 25,
-                                    end: 30,
+                                    end: 35,
                                 },
-                                left: Var {
+                                left: AddInt {
                                     location: SrcSpan {
                                         start: 25,
-                                        end: 26,
-                                    },
-                                    type_: (),
-                                    name: "x",
-                                },
-                                right: Var {
-                                    location: SrcSpan {
-                                        start: 29,
                                         end: 30,
                                     },
-                                    type_: (),
-                                    name: "y",
-                                },
-                            },
-                            right: Constant(
-                                Int {
-                                    location: SrcSpan {
-                                        start: 34,
-                                        end: 35,
+                                    left: Var {
+                                        location: SrcSpan {
+                                            start: 25,
+                                            end: 26,
+                                        },
+                                        type_: (),
+                                        name: "x",
+                                    },
+                                    right: Var {
+                                        location: SrcSpan {
+                                            start: 29,
+                                            end: 30,
+                                        },
+                                        type_: (),
+                                        name: "y",
                                     },
-                                    value: "1",
-                                    int_value: 1,
                                 },
-                            ),
-                        },
-                    ),
-                    then: Var {
-                        location: SrcSpan {
-                            start: 39,
-                            end: 43,
+                                right: Constant(
+                                    Int {
+                                        location: SrcSpan {
+                                            start: 34,
+                                            end: 35,
+                                        },
+                                        value: "1",
+                                        int_value: 1,
+                                    },
+                                ),
+                            },
+                        ),
+                        then: Var {
+                            location: SrcSpan {
+                                start: 39,
+                                end: 43,
+                            },
+                            name: "True",
                         },
-                        name: "True",
                     },
-                },
-            ],
+                ],
+            ),
         },
     ),
 ]
diff --git a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__case_expression_without_body.snap b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__case_expression_without_body.snap
new file mode 100644
index 000000000..78e7ea1aa
--- /dev/null
+++ b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__case_expression_without_body.snap
@@ -0,0 +1,24 @@
+---
+source: compiler-core/src/parse/tests.rs
+expression: case a
+---
+[
+    Expression(
+        Case {
+            location: SrcSpan {
+                start: 0,
+                end: 6,
+            },
+            subjects: [
+                Var {
+                    location: SrcSpan {
+                        start: 5,
+                        end: 6,
+                    },
+                    name: "a",
+                },
+            ],
+            clauses: None,
+        },
+    ),
+]
diff --git a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__case_invalid_expression.snap b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__case_invalid_expression.snap
index 57d959899..03d38fc38 100644
--- a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__case_invalid_expression.snap
+++ b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__case_invalid_expression.snap
@@ -19,5 +19,4 @@ error: Syntax error
   │             ^^^^ I was not expecting this
 
 Found the keyword `type`, expected one of: 
-- `{`
-- an expression
+- `}`
diff --git a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__case_without_body.snap b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__case_without_body.snap
new file mode 100644
index 000000000..c1c396239
--- /dev/null
+++ b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__case_without_body.snap
@@ -0,0 +1,24 @@
+---
+source: compiler-core/src/parse/tests.rs
+expression: case a
+---
+[
+    Expression(
+        Case {
+            location: SrcSpan {
+                start: 0,
+                end: 6,
+            },
+            subjects: [
+                Var {
+                    location: SrcSpan {
+                        start: 5,
+                        end: 6,
+                    },
+                    name: "a",
+                },
+            ],
+            clauses: [],
+        },
+    ),
+]
diff --git a/compiler-core/src/parse/tests.rs b/compiler-core/src/parse/tests.rs
index 8a6bafc54..10538f0d9 100644
--- a/compiler-core/src/parse/tests.rs
+++ b/compiler-core/src/parse/tests.rs
@@ -1678,3 +1678,8 @@ type Wibble {
 fn nested_tuple_access_after_function() {
     assert_parse!("tuple().0.1");
 }
+
+#[test]
+fn case_expression_without_body() {
+    assert_parse!("case a");
+}
diff --git a/compiler-core/src/type_/tests/errors.rs b/compiler-core/src/type_/tests/errors.rs
index 3eb0450d5..e6d009f1e 100644
--- a/compiler-core/src/type_/tests/errors.rs
+++ b/compiler-core/src/type_/tests/errors.rs
@@ -2685,3 +2685,8 @@ fn negative_out_of_range_erlang_float_in_pattern() {
 fn negative_out_of_range_erlang_float_in_const() {
     assert_module_error!(r#"const x = -1.8e308"#);
 }
+
+#[test]
+fn missing_case_body() {
+    assert_error!("case True");
+}
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__missing_case_body.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__missing_case_body.snap
new file mode 100644
index 000000000..8fd7d62de
--- /dev/null
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__missing_case_body.snap
@@ -0,0 +1,15 @@
+---
+source: compiler-core/src/type_/tests/errors.rs
+expression: case True
+---
+----- SOURCE CODE
+case True
+
+----- ERROR
+error: Missing case body
+  ┌─ /src/one/two.gleam:1:1
+  │
+1 │ case True
+  │ ^^^^^^^^^
+
+This case expression is missing its body.
diff --git a/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__src_importing_test.snap b/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__src_importing_test.snap
index 3c1dafe20..3a305e07a 100644
--- a/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__src_importing_test.snap
+++ b/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__src_importing_test.snap
@@ -1,6 +1,5 @@
 ---
 source: test-package-compiler/src/generated_tests.rs
-assertion_line: 273
 expression: "./cases/src_importing_test"
 ---
 error: App importing test module
@@ -11,6 +10,5 @@ error: App importing test module
 
 The application module `one` is importing the test module `two`.
 
-Test modules are not included in production builds so test modules cannot
-import them. Perhaps move the `two` module to the src directory.
-
+Test modules are not included in production builds so application modules
+cannot import them. Perhaps move the `two` module to the src directory.