Repository: gleam-lang/gleam (rust)

Issue:
Hovering over a pipeline suggests the "Add type annotation" code action
(Gleam nightly)

Before:

```gleam
pub fn main() {
  5
  |> Ok
}
```

Hovering over the `5` suggests a "Add type annotation" code action.

After:

```gleam
pub fn main() {
  5: Int
  |> Ok
}
```

I inspected the AST a little bit since I was suspecting this being caused by my own changes doing something similar. `AddAnnotation::visit_typed_assignment` will be called on the generated `_pipe` variable inside of the pipeline, which then passes all the checks and adds a possible edit. ~ :purple_heart: 

Pull request:
Fix Add Annotations action for generated assignments
Fixes #3789 
This PR no longer offers a code action to add annotations for generated assignments from expressions such as pipes and `use`.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9d7315272..e4aa3683c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -386,6 +386,10 @@
   annotation of a parameter of an anonymous function would do nothing.
   ([Surya Rose](https://github.com/GearsDatapacks))
 
+- Fixed a bug where referencing record constructors in JavaScript guards but
+  not calling them could produce invalid code.
+  ([PgBiel](https://github.com/PgBiel))
+
 ## v1.5.1 - 2024-09-26
 
 ### Bug Fixes
diff --git a/compiler-core/src/ast.rs b/compiler-core/src/ast.rs
index f7e127327..efcff40e3 100644
--- a/compiler-core/src/ast.rs
+++ b/compiler-core/src/ast.rs
@@ -27,7 +27,6 @@ use num_bigint::BigInt;
 use pretty_assertions::assert_eq;
 use vec1::Vec1;
 
-pub const TRY_VARIABLE: &str = "_try";
 pub const PIPE_VARIABLE: &str = "_pipe";
 pub const USE_ASSIGNMENT_VARIABLE: &str = "_use";
 pub const ASSERT_FAIL_VARIABLE: &str = "_assert_fail";
@@ -1955,6 +1954,8 @@ pub enum AssignmentKind {
     Let,
     // let assert x = ...
     Assert { location: SrcSpan },
+    // For assignments generated by the compiler
+    Generated,
 }
 
 impl AssignmentKind {
@@ -1965,7 +1966,17 @@ impl AssignmentKind {
     pub fn is_assert(&self) -> bool {
         match self {
             Self::Assert { .. } => true,
-            Self::Let => false,
+            Self::Let | Self::Generated => false,
+        }
+    }
+    /// Returns `true` if the assignment kind is [`Generated`].
+    ///
+    /// [`Generated`]: AssignmentKind::Generated
+    #[must_use]
+    pub fn is_generated(&self) -> bool {
+        match self {
+            Self::Generated => true,
+            Self::Let | Self::Assert { .. } => false,
         }
     }
 }
diff --git a/compiler-core/src/erlang.rs b/compiler-core/src/erlang.rs
index 85e9abb5e..30d3ebca6 100644
--- a/compiler-core/src/erlang.rs
+++ b/compiler-core/src/erlang.rs
@@ -1885,7 +1885,9 @@ fn pipeline<'a>(
 
 fn assignment<'a>(assignment: &'a TypedAssignment, env: &mut Env<'a>) -> Document<'a> {
     match assignment.kind {
-        AssignmentKind::Let => let_(&assignment.value, &assignment.pattern, env),
+        AssignmentKind::Let | AssignmentKind::Generated => {
+            let_(&assignment.value, &assignment.pattern, env)
+        }
         AssignmentKind::Assert { .. } => let_assert(&assignment.value, &assignment.pattern, env),
     }
 }
diff --git a/compiler-core/src/format.rs b/compiler-core/src/format.rs
index c7eae322f..21e4e2d07 100644
--- a/compiler-core/src/format.rs
+++ b/compiler-core/src/format.rs
@@ -897,7 +897,7 @@ impl<'comments> Formatter<'comments> {
         let _ = self.pop_empty_lines(pattern.location().end);
 
         let keyword = match kind {
-            AssignmentKind::Let => "let ",
+            AssignmentKind::Let | AssignmentKind::Generated => "let ",
             AssignmentKind::Assert { .. } => "let assert ",
         };
 
diff --git a/compiler-core/src/javascript/expression.rs b/compiler-core/src/javascript/expression.rs
index 50789ecfd..5892fabe5 100644
--- a/compiler-core/src/javascript/expression.rs
+++ b/compiler-core/src/javascript/expression.rs
@@ -1240,6 +1240,23 @@ pub(crate) fn guard_constant_expression<'a>(
                     tracker.error_used = true;
                 }
             }
+
+            // If there's no arguments and the type is a function that takes
+            // arguments then this is the constructor being referenced, not the
+            // function being called.
+            if let Some(arity) = type_.fn_arity() {
+                if args.is_empty() && arity != 0 {
+                    let arity = arity as u16;
+                    return Ok(record_constructor(
+                        type_.clone(),
+                        None,
+                        name,
+                        arity,
+                        tracker,
+                    ));
+                }
+            }
+
             let field_values: Vec<_> = args
                 .iter()
                 .map(|arg| guard_constant_expression(assignments, tracker, &arg.value))
diff --git a/compiler-core/src/language_server/code_action.rs b/compiler-core/src/language_server/code_action.rs
index c359712e8..041d7b87f 100644
--- a/compiler-core/src/language_server/code_action.rs
+++ b/compiler-core/src/language_server/code_action.rs
@@ -944,6 +944,12 @@ impl<'ast> ast::visit::Visit<'ast> for AddAnnotations<'_> {
             return;
         }
 
+        // Various expressions such as pipelines and `use` expressions generate assignments
+        // internally. However, these cannot be annotated and so we don't offer a code action here.
+        if assignment.kind.is_generated() {
+            return;
+        }
+
         let insert_location = SrcSpan::new(pattern_location.end, pattern_location.end);
         let range = src_span_to_lsp_range(insert_location, &self.line_numbers);
 
diff --git a/compiler-core/src/type_/expression.rs b/compiler-core/src/type_/expression.rs
index 5ed9eff03..cbcadfdff 100644
--- a/compiler-core/src/type_/expression.rs
+++ b/compiler-core/src/type_/expression.rs
@@ -1344,6 +1344,8 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
         // Do not perform exhaustiveness checking if user explicitly used `let assert ... = ...`.
         let exhaustiveness_check = self.check_let_exhaustiveness(location, value.type_(), &pattern);
         match (kind, exhaustiveness_check) {
+            // Generated assignments should be checked before they are generated
+            (AssignmentKind::Generated, _) => {}
             (AssignmentKind::Let, Ok(_)) => {}
             (AssignmentKind::Let, Err(e)) => {
                 self.problems.error(e);
@@ -3835,7 +3837,7 @@ impl UseAssignments {
                         location,
                         pattern,
                         annotation,
-                        kind: AssignmentKind::Let,
+                        kind: AssignmentKind::Generated,
                         value: Box::new(UntypedExpr::Var { location, name }),
                     };
                     assignments
diff --git a/compiler-core/src/type_/pipe.rs b/compiler-core/src/type_/pipe.rs
index 9a67846e7..ed90ce704 100644
--- a/compiler-core/src/type_/pipe.rs
+++ b/compiler-core/src/type_/pipe.rs
@@ -208,7 +208,7 @@ impl<'a, 'b, 'c> PipeTyper<'a, 'b, 'c> {
         let assignment = Assignment {
             location,
             annotation: None,
-            kind: AssignmentKind::Let,
+            kind: AssignmentKind::Generated,
             pattern: Pattern::Variable {
                 location,
                 name: PIPE_VARIABLE.into(),


Test patch:
diff --git a/compiler-core/src/javascript/tests/case_clause_guards.rs b/compiler-core/src/javascript/tests/case_clause_guards.rs
index 9abf60f90..cf24d633a 100644
--- a/compiler-core/src/javascript/tests/case_clause_guards.rs
+++ b/compiler-core/src/javascript/tests/case_clause_guards.rs
@@ -517,3 +517,17 @@ fn func(x) {
 "#,
     );
 }
+
+// Variant of https://github.com/lpil/decode/pull/6
+#[test]
+fn constructor_function_in_guard() {
+    assert_js!(
+        r#"fn func(x) {
+    case [] {
+        _ if [] == [ Ok ] -> True
+        _ -> False
+    }
+}
+    "#,
+    );
+}
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case_clause_guards__constructor_function_in_guard.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case_clause_guards__constructor_function_in_guard.snap
new file mode 100644
index 000000000..d38a54ca9
--- /dev/null
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case_clause_guards__constructor_function_in_guard.snap
@@ -0,0 +1,25 @@
+---
+source: compiler-core/src/javascript/tests/case_clause_guards.rs
+expression: "fn func(x) {\n    case [] {\n        _ if [] == [ Ok ] -> True\n        _ -> False\n    }\n}\n    "
+snapshot_kind: text
+---
+----- SOURCE CODE
+fn func(x) {
+    case [] {
+        _ if [] == [ Ok ] -> True
+        _ -> False
+    }
+}
+    
+
+----- COMPILED JAVASCRIPT
+import { Ok, toList, isEqual } from "../gleam.mjs";
+
+function func(x) {
+  let $ = toList([]);
+  if (isEqual(toList([]), toList([(var0) => { return new Ok(var0); }]))) {
+    return true;
+  } else {
+    return false;
+  }
+}
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case_clause_guards__imported_aliased_ok.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case_clause_guards__imported_aliased_ok.snap
index c3ed71a13..c2d355f44 100644
--- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case_clause_guards__imported_aliased_ok.snap
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case_clause_guards__imported_aliased_ok.snap
@@ -1,6 +1,7 @@
 ---
 source: compiler-core/src/javascript/tests/case_clause_guards.rs
 expression: "import gleam.{Ok as Y}\npub type X {\n  Ok\n}\nfn func() {\n  case Y {\n    y if y == Y -> True\n    _ -> False\n  }\n}\n"
+snapshot_kind: text
 ---
 ----- SOURCE CODE
 import gleam.{Ok as Y}
@@ -23,7 +24,7 @@ export class Ok extends $CustomType {}
 
 function func() {
   let $ = (var0) => { return new Y(var0); };
-  if (isEqual($, new Y())) {
+  if (isEqual($, (var0) => { return new Y(var0); })) {
     let y = $;
     return true;
   } else {
diff --git a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case_clause_guards__imported_ok.snap b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case_clause_guards__imported_ok.snap
index 13410f91f..29b7e6de4 100644
--- a/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case_clause_guards__imported_ok.snap
+++ b/compiler-core/src/javascript/tests/snapshots/gleam_core__javascript__tests__case_clause_guards__imported_ok.snap
@@ -1,6 +1,7 @@
 ---
 source: compiler-core/src/javascript/tests/case_clause_guards.rs
 expression: "import gleam\npub type X {\n  Ok\n}\nfn func(x) {\n  case gleam.Ok {\n    _ if [] == [ gleam.Ok ] -> True\n    _ -> False\n  }\n}\n"
+snapshot_kind: text
 ---
 ----- SOURCE CODE
 import gleam
@@ -17,13 +18,13 @@ fn func(x) {
 
 ----- COMPILED JAVASCRIPT
 import * as $gleam from "../gleam.mjs";
-import { toList, CustomType as $CustomType, isEqual } from "../gleam.mjs";
+import { Ok, toList, CustomType as $CustomType, isEqual } from "../gleam.mjs";
 
 export class Ok extends $CustomType {}
 
 function func(x) {
   let $ = (var0) => { return new $gleam.Ok(var0); };
-  if (isEqual(toList([]), toList([new $gleam.Ok()]))) {
+  if (isEqual(toList([]), toList([(var0) => { return new Ok(var0); }]))) {
     return true;
   } else {
     return false;
diff --git a/compiler-core/src/language_server/tests/action.rs b/compiler-core/src/language_server/tests/action.rs
index 486e90b64..84a54cf3f 100644
--- a/compiler-core/src/language_server/tests/action.rs
+++ b/compiler-core/src/language_server/tests/action.rs
@@ -1973,6 +1973,41 @@ pub fn main() {
     );
 }
 
+#[test]
+// https://github.com/gleam-lang/gleam/issues/3789
+fn no_code_actions_to_add_annotations_for_pipe() {
+    assert_no_code_actions!(
+        ADD_ANNOTATION | ADD_ANNOTATIONS,
+        r#"
+fn do_something(a: Int) { a }
+
+pub fn main() {
+  10 |> do_something
+}
+"#,
+        find_position_of("10").select_until(find_position_of("|>"))
+    );
+}
+
+#[test]
+// https://github.com/gleam-lang/gleam/issues/3789#issuecomment-2455805734
+fn add_correct_type_annotation_for_non_variable_use() {
+    assert_code_action!(
+        ADD_ANNOTATION,
+        r#"
+fn usable(f) {
+  f(#(1, 2))
+}
+
+pub fn main() {
+  use #(a, b) <- usable
+  a + b
+}
+"#,
+        find_position_of("use").select_until(find_position_of("b)"))
+    );
+}
+
 #[test]
 fn test_qualified_to_unqualified_import_basic_with_argument() {
     let src = r#"
diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__add_correct_type_annotation_for_non_variable_use.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__add_correct_type_annotation_for_non_variable_use.snap
new file mode 100644
index 000000000..3b63d6ac7
--- /dev/null
+++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__add_correct_type_annotation_for_non_variable_use.snap
@@ -0,0 +1,27 @@
+---
+source: compiler-core/src/language_server/tests/action.rs
+expression: "\nfn usable(f) {\n  f(#(1, 2))\n}\n\npub fn main() {\n  use #(a, b) <- usable\n  a + b\n}\n"
+---
+----- BEFORE ACTION
+
+fn usable(f) {
+  f(#(1, 2))
+}
+
+pub fn main() {
+  use #(a, b) <- usable
+  ▔▔▔▔▔▔▔▔▔↑           
+  a + b
+}
+
+
+----- AFTER ACTION
+
+fn usable(f) {
+  f(#(1, 2))
+}
+
+pub fn main() {
+  use #(a, b): #(Int, Int) <- usable
+  a + b
+}