Repository: gleam-lang/gleam (rust)

Issue:
Make unused variable warning more precise
Right now when a punned variable is unused you always get the same hint on how to ignore it (which is prepend a `_` to the var name). This is not really precise for punned variables:
```gleam
let Wibble(arg:, arg1:) = todo
//               ^^^^^ Warning: unused, you can ignore it with an underscore `_arg1`
```
If one were to blindly apply the compiler's suggestion here they would get a compilation error:
```gleam
let Wibble(arg:, _arg1) = todo
//               ^^^^^ Error: unlabelled arg after labelled one!

// Or even:
let Wibble(arg:, _arg1:) = todo
//                    ^ Error: I was not expecting this!
```
Which could be confusing for a newcomer. Once we improve unused detection using a call graph <https://github.com/gleam-lang/gleam/issues/2634> we could be more precise and change the suggestion for variables brought into scope through punning and provide a valid suggestion like this:
```gleam
let Wibble(arg:, arg1:) = todo
//               ^^^^^ Warning: unused, you can ignore it with an underscore `arg1: _`
```


Pull request description:
Improve unused variable warnings
Fixes #3425
This PR adds extra context to unused variable warnings, allowing them to correctly give hints about how to ignore those variables.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 67e4dc438..084ff403c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -248,9 +248,13 @@
   leading to incorrect errors on incremental builds and in the Language Server.
   ([Surya Rose](https://github.com/GearsDatapacks))
 
- - Fixed a bug where Gleam would be unable to compile to BEAM bytecode if the
-   project path contains a non-ascii character.
-   ([yoshi](https://github.com/joshi-monster))
+- Fixed a bug where Gleam would be unable to compile to BEAM bytecode if the
+  project path contains a non-ascii character.
+  ([yoshi](https://github.com/joshi-monster))
+
+- Fixed a bug where the compiler would display incorrect hints about ignoring
+  unused variables in certain cases.
+  ([Surya Rose](https://github.com/GearsDatapacks))
 
 ## v1.6.1 - 2024-11-19
 
diff --git a/compiler-core/src/ast.rs b/compiler-core/src/ast.rs
index 5b66bf334..9a2d3ea24 100644
--- a/compiler-core/src/ast.rs
+++ b/compiler-core/src/ast.rs
@@ -14,6 +14,7 @@ pub use self::constant::{Constant, TypedConstant, UntypedConstant};
 use crate::analyse::Inferred;
 use crate::build::{Located, Target};
 use crate::parse::SpannedString;
+use crate::type_::error::VariableOrigin;
 use crate::type_::expression::Implementations;
 use crate::type_::printer::Names;
 use crate::type_::{
@@ -1705,6 +1706,7 @@ pub enum Pattern<Type> {
         location: SrcSpan,
         name: EcoString,
         type_: Type,
+        origin: VariableOrigin,
     },
 
     /// A reference to a variable in a bit array. This is always a variable
diff --git a/compiler-core/src/ast/visit.rs b/compiler-core/src/ast/visit.rs
index 5c019b984..3df1b1c0d 100644
--- a/compiler-core/src/ast/visit.rs
+++ b/compiler-core/src/ast/visit.rs
@@ -40,7 +40,10 @@
 
 use crate::{
     analyse::Inferred,
-    type_::{ModuleValueConstructor, PatternConstructor, TypedCallArg, ValueConstructor},
+    type_::{
+        error::VariableOrigin, ModuleValueConstructor, PatternConstructor, TypedCallArg,
+        ValueConstructor,
+    },
 };
 use std::sync::Arc;
 
@@ -332,8 +335,9 @@ pub trait Visit<'ast> {
         location: &'ast SrcSpan,
         name: &'ast EcoString,
         type_: &'ast Arc<Type>,
+        origin: &'ast VariableOrigin,
     ) {
-        visit_typed_pattern_variable(self, location, name, type_);
+        visit_typed_pattern_variable(self, location, name, type_, origin);
     }
 
     fn visit_typed_pattern_var_usage(
@@ -1105,7 +1109,8 @@ where
             location,
             name,
             type_,
-        } => v.visit_typed_pattern_variable(location, name, type_),
+            origin,
+        } => v.visit_typed_pattern_variable(location, name, type_, origin),
         Pattern::VarUsage {
             location,
             name,
@@ -1191,6 +1196,7 @@ pub fn visit_typed_pattern_variable<'a, V>(
     _location: &'a SrcSpan,
     _name: &'a EcoString,
     _type: &'a Arc<Type>,
+    _origin: &'a VariableOrigin,
 ) where
     V: Visit<'a> + ?Sized,
 {
diff --git a/compiler-core/src/ast_folder.rs b/compiler-core/src/ast_folder.rs
index 897c25ec4..8d0c3df23 100644
--- a/compiler-core/src/ast_folder.rs
+++ b/compiler-core/src/ast_folder.rs
@@ -15,6 +15,7 @@ use crate::{
         UntypedUse, UntypedUseAssignment, Use, UseAssignment,
     },
     build::Target,
+    type_::error::VariableOrigin,
 };
 
 #[allow(dead_code)]
@@ -1115,7 +1116,8 @@ pub trait PatternFolder {
                 location,
                 name,
                 type_: (),
-            } => self.fold_pattern_var(location, name),
+                origin,
+            } => self.fold_pattern_var(location, name, origin),
 
             Pattern::VarUsage {
                 location,
@@ -1200,11 +1202,17 @@ pub trait PatternFolder {
         Pattern::String { location, value }
     }
 
-    fn fold_pattern_var(&mut self, location: SrcSpan, name: EcoString) -> UntypedPattern {
+    fn fold_pattern_var(
+        &mut self,
+        location: SrcSpan,
+        name: EcoString,
+        origin: VariableOrigin,
+    ) -> UntypedPattern {
         Pattern::Variable {
             location,
             name,
             type_: (),
+            origin,
         }
     }
 
diff --git a/compiler-core/src/error.rs b/compiler-core/src/error.rs
index 4ebc1f71d..440c26ad5 100644
--- a/compiler-core/src/error.rs
+++ b/compiler-core/src/error.rs
@@ -3121,7 +3121,7 @@ and the final one is the `use` callback function.\n"));
                         }
                     } else {
                         text.push_str("All the arguments have already been supplied, \
-so it cannot take the the `use` callback function as a final argument.\n")
+so it cannot take the `use` callback function as a final argument.\n")
                     };
 
                     text.push_str("\nSee: https://tour.gleam.run/advanced-features/use/");
diff --git a/compiler-core/src/language_server/code_action.rs b/compiler-core/src/language_server/code_action.rs
index 921a37809..85a948342 100644
--- a/compiler-core/src/language_server/code_action.rs
+++ b/compiler-core/src/language_server/code_action.rs
@@ -14,8 +14,10 @@ use crate::{
     line_numbers::LineNumbers,
     parse::extra::ModuleExtra,
     type_::{
-        self, error::ModuleSuggestion, printer::Printer, FieldMap, ModuleValueConstructor, Type,
-        TypedCallArg,
+        self,
+        error::{ModuleSuggestion, VariableOrigin},
+        printer::Printer,
+        FieldMap, ModuleValueConstructor, Type, TypedCallArg,
     },
     Error,
 };
@@ -453,6 +455,7 @@ impl<'ast> ast::visit::Visit<'ast> for PatternVariableFinder {
         _location: &'ast SrcSpan,
         name: &'ast EcoString,
         _type: &'ast Arc<Type>,
+        _origin: &'ast VariableOrigin,
     ) {
         self.pattern_variables.push(name.clone());
     }
diff --git a/compiler-core/src/language_server/completer.rs b/compiler-core/src/language_server/completer.rs
index 67f63cb94..a093a7436 100644
--- a/compiler-core/src/language_server/completer.rs
+++ b/compiler-core/src/language_server/completer.rs
@@ -18,8 +18,9 @@ use crate::{
     io::{BeamCompiler, CommandExecutor, FileSystemReader, FileSystemWriter},
     line_numbers::LineNumbers,
     type_::{
-        self, collapse_links, pretty::Printer, FieldMap, ModuleInterface, PreludeType,
-        RecordAccessor, Type, TypeConstructor, ValueConstructorVariant, PRELUDE_MODULE_NAME,
+        self, collapse_links, error::VariableOrigin, pretty::Printer, FieldMap, ModuleInterface,
+        PreludeType, RecordAccessor, Type, TypeConstructor, ValueConstructorVariant,
+        PRELUDE_MODULE_NAME,
     },
     Result,
 };
@@ -1070,6 +1071,7 @@ impl<'ast> ast::visit::Visit<'ast> for LocalCompletion<'_> {
         _: &'ast ast::SrcSpan,
         name: &'ast EcoString,
         type_: &'ast Arc<Type>,
+        _origin: &'ast VariableOrigin,
     ) {
         self.push_completion(name, type_.clone());
     }
diff --git a/compiler-core/src/parse.rs b/compiler-core/src/parse.rs
index 073dfb51e..0f63620d2 100644
--- a/compiler-core/src/parse.rs
+++ b/compiler-core/src/parse.rs
@@ -68,6 +68,7 @@ use crate::ast::{
 use crate::build::Target;
 use crate::error::wrap;
 use crate::parse::extra::ModuleExtra;
+use crate::type_::error::VariableOrigin;
 use crate::type_::expression::Implementations;
 use crate::type_::Deprecation;
 use crate::warning::{DeprecatedSyntaxWarning, WarningEmitter};
@@ -1123,6 +1124,7 @@ where
                             )
                         }
                         _ => Pattern::Variable {
+                            origin: VariableOrigin::Variable(name.clone()),
                             location: SrcSpan { start, end },
                             name,
                             type_: (),
@@ -1777,6 +1779,7 @@ where
                         location: SrcSpan { start, end },
                         label: Some(name.clone()),
                         value: UntypedPattern::Variable {
+                            origin: VariableOrigin::LabelShorthand(name.clone()),
                             name,
                             location: SrcSpan { start, end },
                             type_: (),
diff --git a/compiler-core/src/type_/environment.rs b/compiler-core/src/type_/environment.rs
index 1a2a05e9d..8b4f4f2bd 100644
--- a/compiler-core/src/type_/environment.rs
+++ b/compiler-core/src/type_/environment.rs
@@ -137,8 +137,9 @@ pub enum EntityKind {
     ImportedValue,
     PrivateType,
     Variable {
-        /// How the variable could be rewritten to ignore it when unused
-        how_to_ignore: Option<EcoString>,
+        /// The origin of the variable so we know how it could be
+        /// rewritten to ignore it when unused.
+        origin: VariableOrigin,
     },
 }
 
@@ -693,10 +694,7 @@ impl Environment<'_> {
                     location,
                 },
                 EntityKind::ImportedValue => Warning::UnusedImportedValue { name, location },
-                EntityKind::Variable { how_to_ignore } => Warning::UnusedVariable {
-                    location,
-                    how_to_ignore,
-                },
+                EntityKind::Variable { origin } => Warning::UnusedVariable { location, origin },
             };
 
             problems.warning(warning);
diff --git a/compiler-core/src/type_/error.rs b/compiler-core/src/type_/error.rs
index 6f6467091..81d3c94d8 100644
--- a/compiler-core/src/type_/error.rs
+++ b/compiler-core/src/type_/error.rs
@@ -646,6 +646,34 @@ impl Named {
     }
 }
 
+#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
+/// The origin of a variable. Used to determine how it can be ignored when unused.
+pub enum VariableOrigin {
+    /// A variable that can be ignored by prefixing with an underscore, `_name`
+    Variable(EcoString),
+    /// A variable from label shorthand syntax, which can be ignored with an underscore: `label: _`
+    LabelShorthand(EcoString),
+    /// A variable from an assignment pattern, which can be ignored by removing `as name`,
+    AssignmentPattern,
+    /// A variable generated by the compiler. This should never need to be ignored
+    Generated,
+}
+
+impl VariableOrigin {
+    pub fn how_to_ignore(&self) -> Option<String> {
+        match self {
+            VariableOrigin::Variable(name) => {
+                Some(format!("You can ignore it with an underscore: `_{name}`."))
+            }
+            VariableOrigin::LabelShorthand(label) => Some(format!(
+                "You can ignore it with an underscore: `{label}: _`."
+            )),
+            VariableOrigin::AssignmentPattern => Some("You can safely remove it.".to_string()),
+            VariableOrigin::Generated => None,
+        }
+    }
+}
+
 #[derive(Debug, Eq, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
 pub enum Warning {
     Todo {
@@ -714,8 +742,7 @@ pub enum Warning {
 
     UnusedVariable {
         location: SrcSpan,
-        /// how the variable could be rewritten to be ignored.
-        how_to_ignore: Option<EcoString>,
+        origin: VariableOrigin,
     },
 
     UnnecessaryDoubleIntNegation {
diff --git a/compiler-core/src/type_/expression.rs b/compiler-core/src/type_/expression.rs
index e0c2a14a3..0585aa6fb 100644
--- a/compiler-core/src/type_/expression.rs
+++ b/compiler-core/src/type_/expression.rs
@@ -2434,6 +2434,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
                 location: record_location,
                 name: RECORD_UPDATE_VARIABLE.into(),
                 type_: record_type.clone(),
+                origin: VariableOrigin::Generated,
             },
             annotation: None,
             kind: AssignmentKind::Generated,
@@ -3654,7 +3655,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
                             body_typer.environment.init_usage(
                                 name.clone(),
                                 EntityKind::Variable {
-                                    how_to_ignore: Some(format!("_{name}").into()),
+                                    origin: VariableOrigin::Variable(name.clone()),
                                 },
                                 arg.location,
                                 body_typer.problems,
diff --git a/compiler-core/src/type_/pattern.rs b/compiler-core/src/type_/pattern.rs
index a91cac10d..29d9ee722 100644
--- a/compiler-core/src/type_/pattern.rs
+++ b/compiler-core/src/type_/pattern.rs
@@ -58,6 +58,7 @@ impl<'a, 'b> PatternTyper<'a, 'b> {
         name: &str,
         type_: Arc<Type>,
         location: SrcSpan,
+        origin: VariableOrigin,
     ) -> Result<(), UnifyError> {
         self.check_name_case(location, &EcoString::from(name), Named::Variable);
 
@@ -66,9 +67,7 @@ impl<'a, 'b> PatternTyper<'a, 'b> {
                 // Register usage for the unused variable detection
                 self.environment.init_usage(
                     name.into(),
-                    EntityKind::Variable {
-                        how_to_ignore: Some(format!("_{name}").into()),
-                    },
+                    EntityKind::Variable { origin },
                     location,
                     self.problems,
                 );
@@ -351,14 +350,20 @@ impl<'a, 'b> PatternTyper<'a, 'b> {
             }
             Pattern::Invalid { location, .. } => Ok(Pattern::Invalid { type_, location }),
 
-            Pattern::Variable { name, location, .. } => {
-                self.insert_variable(&name, type_.clone(), location)
+            Pattern::Variable {
+                name,
+                location,
+                origin,
+                ..
+            } => {
+                self.insert_variable(&name, type_.clone(), location, origin.clone())
                     .map_err(|e| convert_unify_error(e, location))?;
 
                 Ok(Pattern::Variable {
                     type_,
                     name,
                     location,
+                    origin,
                 })
             }
 
@@ -404,14 +409,24 @@ impl<'a, 'b> PatternTyper<'a, 'b> {
 
                 // The left hand side may assign a variable, which is the prefix of the string
                 if let Some((left, left_location)) = &left_side_assignment {
-                    self.insert_variable(left.as_ref(), string(), *left_location)
-                        .map_err(|e| convert_unify_error(e, location))?;
+                    self.insert_variable(
+                        left.as_ref(),
+                        string(),
+                        *left_location,
+                        VariableOrigin::AssignmentPattern,
+                    )
+                    .map_err(|e| convert_unify_error(e, location))?;
                 }
 
                 // The right hand side may assign a variable, which is the suffix of the string
                 if let AssignName::Variable(right) = &right_side_assignment {
-                    self.insert_variable(right.as_ref(), string(), right_location)
-                        .map_err(|e| convert_unify_error(e, location))?;
+                    self.insert_variable(
+                        right.as_ref(),
+                        string(),
+                        right_location,
+                        VariableOrigin::Variable(right.clone()),
+                    )
+                    .map_err(|e| convert_unify_error(e, location))?;
                 } else if let AssignName::Discard(right) = &right_side_assignment {
                     self.check_name_case(right_location, right, Named::Discard);
                 };
@@ -432,8 +447,13 @@ impl<'a, 'b> PatternTyper<'a, 'b> {
                 location,
             } => {
                 let pattern = self.unify(*pattern, type_, subject_variable)?;
-                self.insert_variable(&name, pattern.type_().clone(), location)
-                    .map_err(|e| convert_unify_error(e, pattern.location()))?;
+                self.insert_variable(
+                    &name,
+                    pattern.type_().clone(),
+                    location,
+                    VariableOrigin::AssignmentPattern,
+                )
+                .map_err(|e| convert_unify_error(e, pattern.location()))?;
                 Ok(Pattern::Assign {
                     name,
                     pattern: Box::new(pattern),
diff --git a/compiler-core/src/type_/pipe.rs b/compiler-core/src/type_/pipe.rs
index ed90ce704..ad6ebbba7 100644
--- a/compiler-core/src/type_/pipe.rs
+++ b/compiler-core/src/type_/pipe.rs
@@ -213,6 +213,7 @@ impl<'a, 'b, 'c> PipeTyper<'a, 'b, 'c> {
                 location,
                 name: PIPE_VARIABLE.into(),
                 type_: expression.type_(),
+                origin: VariableOrigin::Generated,
             },
             value: Box::new(expression),
         };
diff --git a/compiler-core/src/warning.rs b/compiler-core/src/warning.rs
index 8b24f6563..83f7aa3db 100644
--- a/compiler-core/src/warning.rs
+++ b/compiler-core/src/warning.rs
@@ -630,15 +630,10 @@ Hint: You can safely remove it.
                     }),
                 },
 
-                type_::Warning::UnusedVariable {
-                    location,
-                    how_to_ignore,
-                } => Diagnostic {
+                type_::Warning::UnusedVariable { location, origin } => Diagnostic {
                     title: "Unused variable".into(),
                     text: "".into(),
-                    hint: how_to_ignore.as_ref().map(|rewrite_as| {
-                        format!("You can ignore it with an underscore: `{rewrite_as}`.")
-                    }),
+                    hint: origin.how_to_ignore(),
                     level: diagnostic::Level::Warning,
                     location: Some(Location {
                         src: src.clone(),


Test patch:
diff --git a/compiler-core/src/exhaustiveness/pattern_tests.rs b/compiler-core/src/exhaustiveness/pattern_tests.rs
index 795f060d5..daa803061 100644
--- a/compiler-core/src/exhaustiveness/pattern_tests.rs
+++ b/compiler-core/src/exhaustiveness/pattern_tests.rs
@@ -1,6 +1,6 @@
 use crate::{
     ast::{SrcSpan, TypedPattern},
-    type_,
+    type_::{self, error::VariableOrigin},
 };
 
 use super::pattern::*;
@@ -77,6 +77,7 @@ fn register_variable() {
         location: SrcSpan::new(123, 456),
         name: "wibble".into(),
         type_: type_::int(),
+        origin: VariableOrigin::Variable("wibble".into()),
     };
     let id = patterns.register(&input);
     assert_eq!(
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 2cd69d03f..700217248 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
@@ -41,6 +41,9 @@ expression: "\ncase 2, 3 {\n    x, y if x + y == 1 -> True\n}"
                             },
                             name: "x",
                             type_: (),
+                            origin: Variable(
+                                "x",
+                            ),
                         },
                         Variable {
                             location: SrcSpan {
@@ -49,6 +52,9 @@ expression: "\ncase 2, 3 {\n    x, y if x + y == 1 -> True\n}"
                             },
                             name: "y",
                             type_: (),
+                            origin: Variable(
+                                "y",
+                            ),
                         },
                     ],
                     alternative_patterns: [],
diff --git a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__deeply_nested_tuples.snap b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__deeply_nested_tuples.snap
index 77e23247d..1d0fe9c4f 100644
--- a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__deeply_nested_tuples.snap
+++ b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__deeply_nested_tuples.snap
@@ -56,6 +56,9 @@ expression: "\nlet tup = #(#(#(#(4))))\n{{{tup.0}.0}.0}.0\n"
                 },
                 name: "tup",
                 type_: (),
+                origin: Variable(
+                    "tup",
+                ),
             },
             kind: Let,
             annotation: None,
diff --git a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__deeply_nested_tuples_no_block.snap b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__deeply_nested_tuples_no_block.snap
index 98968de41..1dccdd76e 100644
--- a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__deeply_nested_tuples_no_block.snap
+++ b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__deeply_nested_tuples_no_block.snap
@@ -56,6 +56,9 @@ expression: "\nlet tup = #(#(#(#(4))))\ntup.0.0.0.0\n"
                 },
                 name: "tup",
                 type_: (),
+                origin: Variable(
+                    "tup",
+                ),
             },
             kind: Let,
             annotation: None,
diff --git a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__inner_single_quote_parses.snap b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__inner_single_quote_parses.snap
index 1de528e57..2e0a64242 100644
--- a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__inner_single_quote_parses.snap
+++ b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__inner_single_quote_parses.snap
@@ -23,6 +23,9 @@ expression: "\nlet a = \"inner 'quotes'\"\n"
                 },
                 name: "a",
                 type_: (),
+                origin: Variable(
+                    "a",
+                ),
             },
             kind: Let,
             annotation: None,
diff --git a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__nested_tuples.snap b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__nested_tuples.snap
index ca4522e44..545f3f199 100644
--- a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__nested_tuples.snap
+++ b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__nested_tuples.snap
@@ -48,6 +48,9 @@ expression: "\nlet tup = #(#(5, 6))\n{tup.0}.1\n"
                 },
                 name: "tup",
                 type_: (),
+                origin: Variable(
+                    "tup",
+                ),
             },
             kind: Let,
             annotation: None,
diff --git a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__nested_tuples_no_block.snap b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__nested_tuples_no_block.snap
index d9e6c2a80..ee53eadc7 100644
--- a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__nested_tuples_no_block.snap
+++ b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__nested_tuples_no_block.snap
@@ -48,6 +48,9 @@ expression: "\nlet tup = #(#(5, 6))\ntup.0.1\n"
                 },
                 name: "tup",
                 type_: (),
+                origin: Variable(
+                    "tup",
+                ),
             },
             kind: Let,
             annotation: None,
diff --git a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__with_let_binding3.snap b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__with_let_binding3.snap
index 204c8570b..a3826c380 100644
--- a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__with_let_binding3.snap
+++ b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__with_let_binding3.snap
@@ -39,6 +39,9 @@ expression: "let assert [x] = [2]"
                         },
                         name: "x",
                         type_: (),
+                        origin: Variable(
+                            "x",
+                        ),
                     },
                 ],
                 tail: None,
diff --git a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__with_let_binding3_and_annotation.snap b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__with_let_binding3_and_annotation.snap
index 62f791890..2ef5ced09 100644
--- a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__with_let_binding3_and_annotation.snap
+++ b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__with_let_binding3_and_annotation.snap
@@ -39,6 +39,9 @@ expression: "let assert [x]: List(Int) = [2]"
                         },
                         name: "x",
                         type_: (),
+                        origin: Variable(
+                            "x",
+                        ),
                     },
                 ],
                 tail: None,
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__use___wrong_arity_more_than_required.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__use___wrong_arity_more_than_required.snap
index b6265b661..f2260e58c 100644
--- a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__use___wrong_arity_more_than_required.snap
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__use___wrong_arity_more_than_required.snap
@@ -17,7 +17,7 @@ error: Incorrect arity
   │        ^^^^^^^ Expected 2 arguments, got 3
 
 The function on the right of `<-` here takes 2 arguments.
-All the arguments have already been supplied, so it cannot take the the
-`use` callback function as a final argument.
+All the arguments have already been supplied, so it cannot take the `use`
+callback function as a final argument.
 
 See: https://tour.gleam.run/advanced-features/use/
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__use___wrong_arity_more_than_required_2.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__use___wrong_arity_more_than_required_2.snap
index e838bc449..b62d6a2f9 100644
--- a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__use___wrong_arity_more_than_required_2.snap
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__use___wrong_arity_more_than_required_2.snap
@@ -17,7 +17,7 @@ error: Incorrect arity
   │        ^^^^^^^^^^ Expected 2 arguments, got 4
 
 The function on the right of `<-` here takes 2 arguments.
-All the arguments have already been supplied, so it cannot take the the
-`use` callback function as a final argument.
+All the arguments have already been supplied, so it cannot take the `use`
+callback function as a final argument.
 
 See: https://tour.gleam.run/advanced-features/use/
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__unused_label_shorthand_pattern_arg.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__unused_label_shorthand_pattern_arg.snap
index 29b223946..ef27ade58 100644
--- a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__unused_label_shorthand_pattern_arg.snap
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__unused_label_shorthand_pattern_arg.snap
@@ -19,4 +19,4 @@ warning: Unused variable
 5 │   let Wibble(arg1:, arg2:) = Wibble(1, True)
   │                     ^^^^^ This variable is never used
 
-Hint: You can ignore it with an underscore: `_arg2`.
+Hint: You can ignore it with an underscore: `arg2: _`.
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__unused_label_shorthand_pattern_arg_shadowing.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__unused_label_shorthand_pattern_arg_shadowing.snap
index 8ac4ba671..523875e18 100644
--- a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__unused_label_shorthand_pattern_arg_shadowing.snap
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__unused_label_shorthand_pattern_arg_shadowing.snap
@@ -20,7 +20,7 @@ warning: Unused variable
 5 │   let Wibble(arg1:, arg2:) = Wibble(1, True)
   │              ^^^^^ This variable is never used
 
-Hint: You can ignore it with an underscore: `_arg1`.
+Hint: You can ignore it with an underscore: `arg1: _`.
 
 warning: Unused variable
   ┌─ /src/warning/wrn.gleam:5:21
@@ -28,4 +28,4 @@ warning: Unused variable
 5 │   let Wibble(arg1:, arg2:) = Wibble(1, True)
   │                     ^^^^^ This variable is never used
 
-Hint: You can ignore it with an underscore: `_arg2`.
+Hint: You can ignore it with an underscore: `arg2: _`.
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__unused_variable_assignment_pattern.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__unused_variable_assignment_pattern.snap
new file mode 100644
index 000000000..5904ebb26
--- /dev/null
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__unused_variable_assignment_pattern.snap
@@ -0,0 +1,24 @@
+---
+source: compiler-core/src/type_/tests/warnings.rs
+expression: "\ntype Wibble {\n  Wibble(a: Int, b: Int)\n}\n\npub fn main() {\n  let Wibble(a:, ..) as w = Wibble(1, 2)\n  a\n}\n"
+---
+----- SOURCE CODE
+
+type Wibble {
+  Wibble(a: Int, b: Int)
+}
+
+pub fn main() {
+  let Wibble(a:, ..) as w = Wibble(1, 2)
+  a
+}
+
+
+----- WARNING
+warning: Unused variable
+  ┌─ /src/warning/wrn.gleam:7:25
+  │
+7 │   let Wibble(a:, ..) as w = Wibble(1, 2)
+  │                         ^ This variable is never used
+
+Hint: You can safely remove it.
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__unused_variable_string_prefix_pattern.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__unused_variable_string_prefix_pattern.snap
new file mode 100644
index 000000000..0df45f4e0
--- /dev/null
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__unused_variable_string_prefix_pattern.snap
@@ -0,0 +1,20 @@
+---
+source: compiler-core/src/type_/tests/warnings.rs
+expression: "\npub fn main() {\n  let assert \"hello\" as hello <> rest = \"hello, world\"\n  rest\n}\n"
+---
+----- SOURCE CODE
+
+pub fn main() {
+  let assert "hello" as hello <> rest = "hello, world"
+  rest
+}
+
+
+----- WARNING
+warning: Unused variable
+  ┌─ /src/warning/wrn.gleam:3:25
+  │
+3 │   let assert "hello" as hello <> rest = "hello, world"
+  │                         ^^^^^ This variable is never used
+
+Hint: You can safely remove it.
diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__unused_variable_string_prefix_pattern2.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__unused_variable_string_prefix_pattern2.snap
new file mode 100644
index 000000000..691a9c3fb
--- /dev/null
+++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__warnings__unused_variable_string_prefix_pattern2.snap
@@ -0,0 +1,20 @@
+---
+source: compiler-core/src/type_/tests/warnings.rs
+expression: "\npub fn main() {\n  let assert \"hello\" as hello <> rest = \"hello, world\"\n  hello\n}\n"
+---
+----- SOURCE CODE
+
+pub fn main() {
+  let assert "hello" as hello <> rest = "hello, world"
+  hello
+}
+
+
+----- WARNING
+warning: Unused variable
+  ┌─ /src/warning/wrn.gleam:3:34
+  │
+3 │   let assert "hello" as hello <> rest = "hello, world"
+  │                                  ^^^^ This variable is never used
+
+Hint: You can ignore it with an underscore: `_rest`.
diff --git a/compiler-core/src/type_/tests/warnings.rs b/compiler-core/src/type_/tests/warnings.rs
index 1367ad206..9ff8ca51c 100644
--- a/compiler-core/src/type_/tests/warnings.rs
+++ b/compiler-core/src/type_/tests/warnings.rs
@@ -2948,3 +2948,44 @@ fn each(list, _fun) { list }
 "#
     );
 }
+
+#[test]
+// https://github.com/gleam-lang/gleam/issues/3425
+fn unused_variable_assignment_pattern() {
+    assert_warning!(
+        "
+type Wibble {
+  Wibble(a: Int, b: Int)
+}
+
+pub fn main() {
+  let Wibble(a:, ..) as w = Wibble(1, 2)
+  a
+}
+"
+    );
+}
+
+#[test]
+fn unused_variable_string_prefix_pattern() {
+    assert_warning!(
+        r#"
+pub fn main() {
+  let assert "hello" as hello <> rest = "hello, world"
+  rest
+}
+"#
+    );
+}
+
+#[test]
+fn unused_variable_string_prefix_pattern2() {
+    assert_warning!(
+        r#"
+pub fn main() {
+  let assert "hello" as hello <> rest = "hello, world"
+  hello
+}
+"#
+    );
+}