Repository: gleam-lang/gleam (rust)

Issue:
"Qualify `x` as `wibble.x` code action does not respect shadowing.
Gleam encourages the reuse of meaningful variable names through shadowing, be that through `let` bindings, pattern matching, or lambda argument names. When triggering the code action to qualify a function with its module name, the transformation is applied to **any** variable that matches the unqualified name, even if it is an unrelated shadowing:

```gleam
import wibble.{x}

fn example() {
  echo x
       ^ place cursor here

  let x = 1

  echo x

  let cb = fn(x) {
    echo x
  }

  todo
}
```

Triggering the code action at the cursor position indicator above results in the following module:

```gleam
import wibble

fn example() {
  echo wibble.x

  let x = 1

  echo wibble.x

  let cb = fn(x) {
    echo wibble.x
  }

  todo
}
```

I would **expect** the module to look like this:

```gleam
import wibble.{x}

fn example() {
  echo wibble.x

  let x = 1

  echo x

  let cb = fn(x) {
    echo x
  }

  todo
}
```

It is important to note that placing my cursor on any of the shadowed `x` uses will **not let me trigger the code action** because the LSP is aware that these are not the same `x`. 

I ran into this refactoring some tests and ended up undoing the action and manually updating things as the amount of incorrect renames was much higher.

Pull request:
avoid qualify code action on shadowed modules
resolves #4548 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d5c1f16a9..eb17e58be 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -411,3 +411,8 @@
 - Only print the user-friendly LSP message when stdin is a terminal to avoid
   emitting redundant logs.
   ([Ariel Parker](https://github.com/arielherself))
+
+- Fixed a bug where the language server would wrongly override local variables
+  if they were shadowing an unqualified module function.
+  ([Samuel Cristobal](https://github.com/scristobal))
+
diff --git a/compiler-core/src/language_server/code_action.rs b/compiler-core/src/language_server/code_action.rs
index c8ede15cf..6525442ed 100644
--- a/compiler-core/src/language_server/code_action.rs
+++ b/compiler-core/src/language_server/code_action.rs
@@ -2221,7 +2221,11 @@ impl<'ast> ast::visit::Visit<'ast> for UnqualifiedToQualifiedImportSecondPass<'a
             layer,
             ..
         } = &self.unqualified_constructor;
-        if layer.is_value() && wanted_constructor.used_name() == name {
+
+        if layer.is_value()
+            && wanted_constructor.used_name() == name
+            && !constructor.is_local_variable()
+        {
             self.add_module_qualifier(*location);
         }
         ast::visit::visit_typed_expr_var(self, location, constructor, name);


Test patch:
diff --git a/compiler-core/src/language_server/tests/action.rs b/compiler-core/src/language_server/tests/action.rs
index cd56db3b0..50f6dea0c 100644
--- a/compiler-core/src/language_server/tests/action.rs
+++ b/compiler-core/src/language_server/tests/action.rs
@@ -3582,6 +3582,37 @@ pub fn main() {
             .select_until(find_position_of("(1)")),
     );
 }
+
+#[test]
+fn test_unqualified_to_qualified_import_variable_shadowing() {
+    let src = r#"
+
+import wibble.{wobble}
+
+pub fn example() {
+  echo wobble
+
+  let wobble = 1
+
+  echo wobble
+
+  let _ = fn(wobble) {
+    echo wobble
+  }
+
+  todo
+}
+"#;
+
+    assert_code_action!(
+        "Qualify wobble as wibble.wobble",
+        TestProject::for_source(src).add_hex_module("wibble", "pub fn wobble() { todo }"),
+        find_position_of("wob")
+            .nth_occurrence(2)
+            .select_until(find_position_of("ble").nth_occurrence(3))
+    );
+}
+
 /* TODO: implement qualified unused location
 #[test]
 fn test_remove_unused_qualified_action() {
diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__unqualified_to_qualified_import_variable_shadowing.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__unqualified_to_qualified_import_variable_shadowing.snap
new file mode 100644
index 000000000..157e6a50d
--- /dev/null
+++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__unqualified_to_qualified_import_variable_shadowing.snap
@@ -0,0 +1,43 @@
+---
+source: compiler-core/src/language_server/tests/action.rs
+expression: "\n\nimport wibble.{wobble}\n\npub fn example() {\n  echo wobble\n\n  let wobble = 1\n\n  echo wobble\n\n  let _ = fn(wobble) {\n    echo wobble\n  }\n\n  todo\n}\n"
+---
+----- BEFORE ACTION
+
+
+import wibble.{wobble}
+
+pub fn example() {
+  echo wobble
+       ▔▔▔↑  
+
+  let wobble = 1
+
+  echo wobble
+
+  let _ = fn(wobble) {
+    echo wobble
+  }
+
+  todo
+}
+
+
+----- AFTER ACTION
+
+
+import wibble.{}
+
+pub fn example() {
+  echo wibble.wobble
+
+  let wobble = 1
+
+  echo wobble
+
+  let _ = fn(wobble) {
+    echo wobble
+  }
+
+  todo
+}