Repository: gleam-lang/gleam (rust)

Issue:
Language server reports wrong type for variables
I ran into a strange behaviour on main. If I write the following code:
```gleam
pub type Wibble =
  List(Int)

pub fn main() {
  []
}
```
The inferred type for `main` (and what I get if I let the LS add the type annotation) is `fn() -> Wibble(a)` which is wrong since `Wibble` is not generic and I wouldn't really want it to use the alias even if it were.

It also happens with aliases imported from other modules:
```gleam
// type DecodeErrors = List(DecodeError)
import gleam/dynamic.{type DecodeErrors}

// Inferred as `fn() -> DecodeErrors(a)`
pub fn main() {
  []
}
```

---

edit: another really strange thing is if I add the type explicitly it still adds the type variable. For example:

```gleam
pub type Wibble =
  List(Int)

// Inferred as `fn(Wibble(Int)) -> Nil`
fn wibble(a: Wibble) {
  Nil
}
```


Pull request description:
Fix bug with type alias printing
Fixes #3749 
This PR changes the `Names` class to only register aliases when the type parameters map exactly to those of the type they alias. For example:
```gleam
type MyResult(a, b) = Result(a, b) // Result will now be printed as MyResult
type OptionalInt = option.Option(Int) // This is ignored as it doesn't map nicely
```

In future we could implement a more complex system, which could map the arguments of the type to the parameters of the alias, but I started implementing that and it got pretty complex pretty fast, so I just went with this minimal approach which fixes the bug.

Patch:
diff --git a/compiler-core/src/analyse.rs b/compiler-core/src/analyse.rs
index 4b5c39cbd..c75f2eb5a 100644
--- a/compiler-core/src/analyse.rs
+++ b/compiler-core/src/analyse.rs
@@ -1182,7 +1182,7 @@ impl<'a, A> ModuleAnalyzer<'a, A> {
 
             environment
                 .names
-                .type_in_scope(name.clone(), type_.as_ref());
+                .type_in_scope(name.clone(), type_.as_ref(), &parameters);
 
             // Insert the alias so that it can be used by other code.
             environment.insert_type_constructor(
diff --git a/compiler-core/src/analyse/imports.rs b/compiler-core/src/analyse/imports.rs
index ab5c4f83c..d01349ee1 100644
--- a/compiler-core/src/analyse/imports.rs
+++ b/compiler-core/src/analyse/imports.rs
@@ -95,9 +95,11 @@ impl<'context, 'problems> Importer<'context, 'problems> {
 
         let type_info = type_info.clone().with_location(import.location);
 
-        self.environment
-            .names
-            .type_in_scope(imported_name.clone(), type_info.type_.as_ref());
+        self.environment.names.type_in_scope(
+            imported_name.clone(),
+            type_info.type_.as_ref(),
+            &type_info.parameters,
+        );
 
         if let Err(e) = self
             .environment
diff --git a/compiler-core/src/type_/printer.rs b/compiler-core/src/type_/printer.rs
index 91b50d3d5..6bf053dde 100644
--- a/compiler-core/src/type_/printer.rs
+++ b/compiler-core/src/type_/printer.rs
@@ -3,7 +3,7 @@ use ecow::EcoString;
 use im::HashMap;
 use std::{collections::HashSet, sync::Arc};
 
-use crate::type_::{Type, TypeVar};
+use crate::type_::{collapse_links, Type, TypeVar};
 
 /// This class keeps track of what names are used for modules in the current
 /// scope, so they can be printed in errors, etc.
@@ -116,6 +116,21 @@ pub struct Names {
     local_value_constructors: BiMap<(EcoString, EcoString), EcoString>,
 }
 
+/// The `PartialEq` implementation for `Type` doesn't account for `TypeVar::Link`,
+/// so we implement an equality check that does account for it here.
+fn compare_arguments(arguments: &[Arc<Type>], parameters: &[Arc<Type>]) -> bool {
+    if arguments.len() != parameters.len() {
+        return false;
+    }
+
+    arguments
+        .iter()
+        .zip(parameters)
+        .all(|(argument, parameter)| {
+            collapse_links(argument.clone()) == collapse_links(parameter.clone())
+        })
+}
+
 impl Names {
     pub fn new() -> Self {
         Self {
@@ -139,12 +154,19 @@ impl Names {
             .insert((module_name, type_name), local_alias);
     }
 
-    pub fn type_in_scope(&mut self, local_alias: EcoString, type_: &Type) {
+    pub fn type_in_scope(
+        &mut self,
+        local_alias: EcoString,
+        type_: &Type,
+        parameters: &[Arc<Type>],
+    ) {
         match type_ {
-            Type::Named { module, name, .. } => {
+            Type::Named {
+                module, name, args, ..
+            } if compare_arguments(args, parameters) => {
                 self.named_type_in_scope(module.clone(), name.clone(), local_alias);
             }
-            Type::Fn { .. } | Type::Var { .. } | Type::Tuple { .. } => {
+            Type::Named { .. } | Type::Fn { .. } | Type::Var { .. } | Type::Tuple { .. } => {
                 _ = self.local_types.remove_by_right(&local_alias);
             }
         }


Test patch:
diff --git a/compiler-core/src/language_server/tests/hover.rs b/compiler-core/src/language_server/tests/hover.rs
index cbf17b2d3..9bc98cc64 100644
--- a/compiler-core/src/language_server/tests/hover.rs
+++ b/compiler-core/src/language_server/tests/hover.rs
@@ -1169,6 +1169,53 @@ fn do_thing() -> LocalResult {
     );
 }
 
+#[test]
+fn hover_print_alias_when_parameters_match() {
+    let code = "
+type MyResult(a, b) = Result(a, b)
+
+fn do_thing() -> MyResult(Int, Int) {
+  Error(1)
+}
+";
+
+    assert_hover!(
+        TestProject::for_source(code),
+        find_position_of("do_thing").under_char('d')
+    );
+}
+
+#[test]
+fn hover_print_underlying_for_imported_alias() {
+    let code = "
+import alias.{type A}
+
+fn wibble() -> Result(Int, String) {
+  todo
+}
+";
+
+    assert_hover!(
+        TestProject::for_source(code).add_hex_module("alias", "pub type A = Result(Int, String)"),
+        find_position_of("wibble").under_char('l')
+    );
+}
+
+#[test]
+fn hover_print_aliased_imported_generic_type() {
+    let code = "
+import gleam/option.{type Option as Maybe}
+
+const none: Maybe(Int) = option.None
+";
+
+    assert_hover!(
+        TestProject::for_source(code)
+            .add_hex_module("gleam/option", "pub type Option(a) { None Some(a) }"),
+        find_position_of("none").under_char('e')
+    );
+}
+
 #[test]
 fn hover_print_qualified_prelude_type_when_shadowed_by_alias() {
     let code = "
diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__hover__hover_print_alias_when_parameters_match.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__hover__hover_print_alias_when_parameters_match.snap
new file mode 100644
index 000000000..0d44122d0
--- /dev/null
+++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__hover__hover_print_alias_when_parameters_match.snap
@@ -0,0 +1,18 @@
+---
+source: compiler-core/src/language_server/tests/hover.rs
+expression: "\ntype MyResult(a, b) = Result(a, b)\n\nfn do_thing() -> MyResult(Int, Int) {\n  Error(1)\n}\n"
+---
+type MyResult(a, b) = Result(a, b)
+
+fn do_thing() -> MyResult(Int, Int) {
+▔▔▔↑▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔  
+  Error(1)
+}
+
+
+----- Hover content -----
+Scalar(
+    String(
+        "```gleam\nfn() -> MyResult(Int, Int)\n```\n",
+    ),
+)
diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__hover__hover_print_aliased_imported_generic_type.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__hover__hover_print_aliased_imported_generic_type.snap
new file mode 100644
index 000000000..46820fd71
--- /dev/null
+++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__hover__hover_print_aliased_imported_generic_type.snap
@@ -0,0 +1,16 @@
+---
+source: compiler-core/src/language_server/tests/hover.rs
+expression: "\nimport gleam/option.{type Option as Maybe}\n\nconst none: Maybe(Int) = option.None\n"
+---
+import gleam/option.{type Option as Maybe}
+
+const none: Maybe(Int) = option.None
+▔▔▔▔▔▔▔▔▔↑▔▔▔▔▔▔▔▔▔▔▔▔              
+
+
+----- Hover content -----
+Scalar(
+    String(
+        "```gleam\nMaybe(Int)\n```\n",
+    ),
+)
diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__hover__hover_print_underlying_for_alias_with_parameters.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__hover__hover_print_underlying_for_alias_with_parameters.snap
index 051d736fa..4c550e6b6 100644
--- a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__hover__hover_print_underlying_for_alias_with_parameters.snap
+++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__hover__hover_print_underlying_for_alias_with_parameters.snap
@@ -1,6 +1,5 @@
 ---
 source: compiler-core/src/language_server/tests/hover.rs
-assertion_line: 1166
 expression: "\ntype LocalResult = Result(String, Int)\n\nfn do_thing() -> LocalResult {\n  Error(1)\n}\n"
 ---
 type LocalResult = Result(String, Int)
@@ -14,6 +13,6 @@ fn do_thing() -> LocalResult {
 ----- Hover content -----
 Scalar(
     String(
-        "```gleam\nfn() -> LocalResult(String, Int)\n```\n",
+        "```gleam\nfn() -> Result(String, Int)\n```\n",
     ),
 )
diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__hover__hover_print_underlying_for_imported_alias.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__hover__hover_print_underlying_for_imported_alias.snap
new file mode 100644
index 000000000..31f374456
--- /dev/null
+++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__hover__hover_print_underlying_for_imported_alias.snap
@@ -0,0 +1,18 @@
+---
+source: compiler-core/src/language_server/tests/hover.rs
+expression: "\nimport alias.{type A}\n\nfn wibble() -> Result(Int, String) {\n  todo\n}\n"
+---
+import alias.{type A}
+
+fn wibble() -> Result(Int, String) {
+▔▔▔▔▔▔▔↑▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔  
+  todo
+}
+
+
+----- Hover content -----
+Scalar(
+    String(
+        "```gleam\nfn() -> Result(Int, String)\n```\n",
+    ),
+)