Repository: gleam-lang/gleam (rust)

Issue:
"Extract constant" code action extracts functions
With this code:
```gleam
pub fn main() {
  io.println("Hello, world")
  // ^ trigger code action
}
```
The LSP offers an "Extract Constant" code action to perform the following transformation:
```gleam
const nil = io.println("Hello, world")

pub fn main() {
  nil
}
```
Which is invalid code, functions cannot appear in constants

Pull request:
Fix extracting constants of functions
Fixes #4427

Patch:
diff --git a/compiler-cli/src/dependencies.rs b/compiler-cli/src/dependencies.rs
index d8bb9d77c..b9728547b 100644
--- a/compiler-cli/src/dependencies.rs
+++ b/compiler-cli/src/dependencies.rs
@@ -1014,7 +1014,7 @@ fn provide_package(
     if parents.contains(&package_name) {
         let mut last_cycle = parents
             .split(|p| p == &package_name)
-            .last()
+            .next_back()
             .unwrap_or_default()
             .to_vec();
         last_cycle.push(package_name);
diff --git a/compiler-core/src/ast.rs b/compiler-core/src/ast.rs
index 89f4ba126..6b27c066f 100644
--- a/compiler-core/src/ast.rs
+++ b/compiler-core/src/ast.rs
@@ -726,7 +726,7 @@ impl<T> Import<T> {
         match self.as_name.as_ref() {
             Some((AssignName::Variable(name), _)) => Some(name.clone()),
             Some((AssignName::Discard(_), _)) => None,
-            None => self.module.split('/').last().map(EcoString::from),
+            None => self.module.split('/').next_back().map(EcoString::from),
         }
     }
 
diff --git a/compiler-core/src/dependency.rs b/compiler-core/src/dependency.rs
index 3d2f810fd..c6b58cd3b 100644
--- a/compiler-core/src/dependency.rs
+++ b/compiler-core/src/dependency.rs
@@ -72,11 +72,7 @@ fn parse_exact_version(ver: &str) -> Option<Version> {
     if version.starts_with("==") || first_byte.is_some_and(|v| v.is_ascii_digit()) {
         let version = version.replace("==", "");
         let version = version.as_str().trim();
-        if let Ok(v) = Version::parse(version) {
-            Some(v)
-        } else {
-            None
-        }
+        Version::parse(version).ok()
     } else {
         None
     }
diff --git a/compiler-core/src/error.rs b/compiler-core/src/error.rs
index cad477f6a..eceaa0c3c 100644
--- a/compiler-core/src/error.rs
+++ b/compiler-core/src/error.rs
@@ -789,7 +789,7 @@ fn edit_distance_with_substrings(a: &str, b: &str, limit: usize) -> Option<usize
         1 // Exact substring match, but not a total word match so return non-zero
     } else if !big_len_diff {
         // Not a big difference in length, discount cost of length difference
-        score + (len_diff + 1) / 2
+        score + len_diff.div_ceil(2)
     } else {
         // A big difference in length, add back the difference in length to the score
         score + len_diff
diff --git a/compiler-core/src/exhaustiveness.rs b/compiler-core/src/exhaustiveness.rs
index e7c63677e..93d601f4f 100644
--- a/compiler-core/src/exhaustiveness.rs
+++ b/compiler-core/src/exhaustiveness.rs
@@ -235,7 +235,6 @@ impl Branch {
                     Pattern::Assign { name, pattern } => {
                         self.body.assign(name.clone(), check.var.clone());
                         check.pattern = *pattern;
-                        continue;
                     }
                     // There's a special case of assignments when it comes to string
                     // prefix patterns. We can give a name to a literal prefix like this:
diff --git a/compiler-core/src/exhaustiveness/printer.rs b/compiler-core/src/exhaustiveness/printer.rs
index 07b952e02..13631023c 100644
--- a/compiler-core/src/exhaustiveness/printer.rs
+++ b/compiler-core/src/exhaustiveness/printer.rs
@@ -57,7 +57,7 @@ impl<'a> Printer<'a> {
                     NameContextInformation::Qualified(m, n) => (Some(m), n),
                     NameContextInformation::Unqualified(n) => (None, n),
                     NameContextInformation::Unimported(n) => {
-                        (Some(module.split('/').last().unwrap_or(module)), n)
+                        (Some(module.split('/').next_back().unwrap_or(module)), n)
                     }
                 };
 
diff --git a/compiler-core/src/format.rs b/compiler-core/src/format.rs
index f11b08c33..6a79db474 100644
--- a/compiler-core/src/format.rs
+++ b/compiler-core/src/format.rs
@@ -412,7 +412,7 @@ impl<'comments> Formatter<'comments> {
                 };
 
                 let doc = docvec!["import ", module.as_str(), second];
-                let default_module_access_name = module.split('/').last().map(EcoString::from);
+                let default_module_access_name = module.split('/').next_back().map(EcoString::from);
                 match (default_module_access_name, as_name) {
                     // If the `as name` is the same as the module name that would be
                     // used anyways we won't render it. For example:
diff --git a/compiler-core/src/javascript.rs b/compiler-core/src/javascript.rs
index 26488fd22..d41e3aa9c 100644
--- a/compiler-core/src/javascript.rs
+++ b/compiler-core/src/javascript.rs
@@ -90,7 +90,7 @@ impl<'a> Generator<'a> {
             .name
             .as_str()
             .split('/')
-            .last()
+            .next_back()
             .expect("JavaScript generator could not identify imported module name.");
 
         docvec!["/// <reference types=\"./", module, ".d.mts\" />", line()]
@@ -453,7 +453,7 @@ impl<'a> Generator<'a> {
         let get_name = |module: &'a str| {
             module
                 .split('/')
-                .last()
+                .next_back()
                 .expect("JavaScript generator could not identify imported module name.")
         };
 
diff --git a/compiler-core/src/javascript/typescript.rs b/compiler-core/src/javascript/typescript.rs
index b8a0230b8..76eb3d6f4 100644
--- a/compiler-core/src/javascript/typescript.rs
+++ b/compiler-core/src/javascript/typescript.rs
@@ -626,7 +626,7 @@ impl<'a> TypeScriptGenerator<'a> {
 
         let name = match self.aliased_module_names.get(name) {
             Some(name) => name,
-            None => name.split('/').last().expect("Non empty module path"),
+            None => name.split('/').next_back().expect("Non empty module path"),
         };
 
         eco_format!("${name}")
diff --git a/compiler-core/src/language_server/code_action.rs b/compiler-core/src/language_server/code_action.rs
index 9b3df744c..96bb21f22 100644
--- a/compiler-core/src/language_server/code_action.rs
+++ b/compiler-core/src/language_server/code_action.rs
@@ -275,7 +275,7 @@ impl<'a> RedundantTupleInCaseSubject<'a> {
         // tuple items.
         self.edits.replace(
             discard_location,
-            itertools::intersperse(iter::repeat("_").take(tuple_items), ", ").collect(),
+            itertools::intersperse(iter::repeat_n("_", tuple_items), ", ").collect(),
         )
     }
 }
@@ -724,7 +724,7 @@ impl<'a> FillInMissingLabelledArgs<'a> {
             //
             let label_insertion_start = call_location.end - 1;
             let has_comma_after_last_argument =
-                if let Some(last_arg) = args.iter().filter(|arg| !arg.is_implicit()).last() {
+                if let Some(last_arg) = args.iter().filter(|arg| !arg.is_implicit()).next_back() {
                     self.module
                         .code
                         .get(last_arg.location.end as usize..=label_insertion_start as usize)
@@ -2310,7 +2310,7 @@ impl<'a> ConvertFromUse<'a> {
             .get(use_line_end as usize - 1..use_line_end as usize)
             == Some(")");
 
-        let last_explicit_arg = args.iter().filter(|arg| !arg.is_implicit()).last();
+        let last_explicit_arg = args.iter().filter(|arg| !arg.is_implicit()).next_back();
         let last_arg_end = last_explicit_arg.map_or(use_line_end - 1, |arg| arg.location.end);
 
         // This is the piece of code between the end of the last argument and
@@ -3023,8 +3023,8 @@ fn can_be_constant(
         }
 
         // Extract record types as long as arguments can be constant
-        TypedExpr::Call { args, type_, .. } => {
-            matches!(type_.as_ref(), Type::Named { .. })
+        TypedExpr::Call { args, fun, .. } => {
+            fun.is_record_builder()
                 && args
                     .iter()
                     .all(|arg| can_be_constant(module, &arg.value, module_constants))
@@ -4868,7 +4868,7 @@ fn labels_are_correct(args: &[TypedCallArg]) -> bool {
                 labelled_arg_found = true;
                 let _ = used_labels.insert(label);
             }
-            None => continue,
+            None => {}
         }
     }
 
@@ -5849,7 +5849,7 @@ impl<'ast> ast::visit::Visit<'ast> for FillUnusedFields<'ast> {
                 let last_argument_end = arguments
                     .iter()
                     .filter(|arg| !arg.is_implicit())
-                    .last()
+                    .next_back()
                     .map(|arg| arg.location.end);
 
                 self.data = Some(FillUnusedFieldsData {
diff --git a/compiler-core/src/language_server/completer.rs b/compiler-core/src/language_server/completer.rs
index 4b606411e..9b194c7ad 100644
--- a/compiler-core/src/language_server/completer.rs
+++ b/compiler-core/src/language_server/completer.rs
@@ -469,16 +469,15 @@ where
             // e.x. when the user has typed mymodule.| we know unqualified module types are no longer relevant.
             if module_select.is_none() {
                 for unqualified in &import.unqualified_types {
-                    match module.get_public_type(&unqualified.name) {
-                        Some(type_) => completions.push(type_completion(
+                    if let Some(type_) = module.get_public_type(&unqualified.name) {
+                        completions.push(type_completion(
                             None,
                             unqualified.used_name(),
                             type_,
                             insert_range,
                             TypeCompletionForm::Default,
                             CompletionKind::ImportedModule,
-                        )),
-                        None => continue,
+                        ))
                     }
                 }
             }
@@ -504,7 +503,7 @@ where
 
             let qualifier = module_full_name
                 .split('/')
-                .last()
+                .next_back()
                 .unwrap_or(module_full_name);
 
             // If the user has already started a module select then don't show irrelevant modules.
@@ -657,19 +656,16 @@ where
             // e.x. when the user has typed mymodule.| we know unqualified module values are no longer relevant.
             if module_select.is_none() {
                 for unqualified in &import.unqualified_values {
-                    match module.get_public_value(&unqualified.name) {
-                        Some(value) => {
-                            let name = unqualified.used_name();
-                            completions.push(value_completion(
-                                None,
-                                mod_name,
-                                name,
-                                value,
-                                insert_range,
-                                CompletionKind::ImportedModule,
-                            ))
-                        }
-                        None => continue,
+                    if let Some(value) = module.get_public_value(&unqualified.name) {
+                        let name = unqualified.used_name();
+                        completions.push(value_completion(
+                            None,
+                            mod_name,
+                            name,
+                            value,
+                            insert_range,
+                            CompletionKind::ImportedModule,
+                        ))
                     }
                 }
             }
@@ -693,7 +689,7 @@ where
             }
             let qualifier = module_full_name
                 .split('/')
-                .last()
+                .next_back()
                 .unwrap_or(module_full_name);
 
             // If the user has already started a module select then don't show irrelevant modules.
diff --git a/compiler-core/src/language_server/messages.rs b/compiler-core/src/language_server/messages.rs
index eef0849ef..2d2b7318c 100644
--- a/compiler-core/src/language_server/messages.rs
+++ b/compiler-core/src/language_server/messages.rs
@@ -114,7 +114,7 @@ impl Notification {
                 let params = cast_notification::<DidChangeTextDocument>(notification);
                 let notification = Notification::SourceFileChangedInMemory {
                     path: super::path(&params.text_document.uri),
-                    text: params.content_changes.into_iter().last()?.text,
+                    text: params.content_changes.into_iter().next_back()?.text,
                 };
                 Some(Message::Notification(notification))
             }
@@ -137,7 +137,7 @@ impl Notification {
             "workspace/didChangeWatchedFiles" => {
                 let params = cast_notification::<DidChangeWatchedFiles>(notification);
                 let notification = Notification::ConfigFileChanged {
-                    path: super::path(&params.changes.into_iter().last()?.uri),
+                    path: super::path(&params.changes.into_iter().next_back()?.uri),
                 };
                 Some(Message::Notification(notification))
             }
diff --git a/compiler-core/src/parse.rs b/compiler-core/src/parse.rs
index 7b53f922b..9c59b1d77 100644
--- a/compiler-core/src/parse.rs
+++ b/compiler-core/src/parse.rs
@@ -3388,8 +3388,7 @@ where
                         Token::Name { name } => name,
                         token => {
                             let hint = match (&token, self.tok0.take()) {
-                                (&Token::Fn { .. }, _)
-                                | (&Token::Pub, Some((_, Token::Fn { .. }, _))) => {
+                                (&Token::Fn, _) | (&Token::Pub, Some((_, Token::Fn, _))) => {
                                     let text =
                                         "Gleam is not an object oriented programming language so
 functions are declared separately from types.";
diff --git a/compiler-core/src/type_/environment.rs b/compiler-core/src/type_/environment.rs
index 3fda1ad6f..c68a1727f 100644
--- a/compiler-core/src/type_/environment.rs
+++ b/compiler-core/src/type_/environment.rs
@@ -702,7 +702,7 @@ impl Environment<'_> {
                     // Don't suggest importing modules if they are already imported
                     _ if self
                         .imported_modules
-                        .contains_key(importable.split('/').last().unwrap_or(importable)) =>
+                        .contains_key(importable.split('/').next_back().unwrap_or(importable)) =>
                     {
                         None
                     }
diff --git a/compiler-core/src/type_/error.rs b/compiler-core/src/type_/error.rs
index e0ea90c58..6f4bee455 100644
--- a/compiler-core/src/type_/error.rs
+++ b/compiler-core/src/type_/error.rs
@@ -137,7 +137,7 @@ impl ModuleSuggestion {
     pub fn last_name_component(&self) -> &str {
         match self {
             ModuleSuggestion::Imported(name) | ModuleSuggestion::Importable(name) => {
-                name.split('/').last().unwrap_or(name)
+                name.split('/').next_back().unwrap_or(name)
             }
         }
     }
diff --git a/compiler-core/src/type_/printer.rs b/compiler-core/src/type_/printer.rs
index 425a93976..9464ef489 100644
--- a/compiler-core/src/type_/printer.rs
+++ b/compiler-core/src/type_/printer.rs
@@ -344,7 +344,7 @@ impl<'a> Printer<'a> {
                     // TODO: indicate that the module is not import and as such
                     // needs to be, as well as how.
                     NameContextInformation::Unimported(n) => {
-                        (Some(module.split('/').last().unwrap_or(module)), n)
+                        (Some(module.split('/').next_back().unwrap_or(module)), n)
                     }
                 };
 
@@ -388,7 +388,7 @@ impl<'a> Printer<'a> {
             NameContextInformation::Qualified(module, name) => (Some(module), name),
             NameContextInformation::Unqualified(name) => (None, name),
             NameContextInformation::Unimported(name) => {
-                (Some(module.split('/').last().unwrap_or(module)), name)
+                (Some(module.split('/').next_back().unwrap_or(module)), name)
             }
         };
 


Test patch:
diff --git a/compiler-core/src/language_server/tests/action.rs b/compiler-core/src/language_server/tests/action.rs
index 48a3f7b0e..a16ab4023 100644
--- a/compiler-core/src/language_server/tests/action.rs
+++ b/compiler-core/src/language_server/tests/action.rs
@@ -7239,7 +7239,7 @@ fn wrap_case_clause_with_multiple_patterns_in_block() {
   Water
   Fire
 }
-        
+
   pub fn f(pokemon_type: PokemonType) {
     case pokemon_type {
       Water | Air -> soak()
@@ -7259,7 +7259,7 @@ fn wrap_case_clause_inside_assignment_in_block() {
   Water
   Fire
 }
-        
+
   pub fn f(pokemon_type: PokemonType) {
     let damage = case pokemon_type {
       Water -> soak()
@@ -7366,3 +7366,21 @@ fn do_not_wrap_assignment_value_in_block() {
         find_position_of("var").to_selection()
     );
 }
+
+// https://github.com/gleam-lang/gleam/issues/4427
+#[test]
+fn extract_constant_function() {
+    assert_no_code_actions!(
+        EXTRACT_CONSTANT,
+        r#"
+fn print(x) {
+  Nil
+}
+
+pub fn main() {
+  print("Hello")
+}
+"#,
+        find_position_of("print").nth_occurrence(2).to_selection()
+    );
+}
diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_case_clause_inside_assignment_in_block.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_case_clause_inside_assignment_in_block.snap
index 0ad56dbb5..16e735ef9 100644
--- a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_case_clause_inside_assignment_in_block.snap
+++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_case_clause_inside_assignment_in_block.snap
@@ -1,6 +1,6 @@
 ---
 source: compiler-core/src/language_server/tests/action.rs
-expression: "pub type PokemonType {\n  Air\n  Water\n  Fire\n}\n        \n  pub fn f(pokemon_type: PokemonType) {\n    let damage = case pokemon_type {\n      Water -> soak()\n      Fire -> burn()\n    }\n\n    \"Pokemon did \" <> damage\n  }"
+expression: "pub type PokemonType {\n  Air\n  Water\n  Fire\n}\n\n  pub fn f(pokemon_type: PokemonType) {\n    let damage = case pokemon_type {\n      Water -> soak()\n      Fire -> burn()\n    }\n\n    \"Pokemon did \" <> damage\n  }"
 ---
 ----- BEFORE ACTION
 pub type PokemonType {
@@ -8,7 +8,7 @@ pub type PokemonType {
   Water
   Fire
 }
-        
+
   pub fn f(pokemon_type: PokemonType) {
     let damage = case pokemon_type {
       Water -> soak()
@@ -26,7 +26,7 @@ pub type PokemonType {
   Water
   Fire
 }
-        
+
   pub fn f(pokemon_type: PokemonType) {
     let damage = case pokemon_type {
       Water -> soak()
diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_case_clause_with_multiple_patterns_in_block.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_case_clause_with_multiple_patterns_in_block.snap
index 815f3fbb8..28574783f 100644
--- a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_case_clause_with_multiple_patterns_in_block.snap
+++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_case_clause_with_multiple_patterns_in_block.snap
@@ -1,6 +1,6 @@
 ---
 source: compiler-core/src/language_server/tests/action.rs
-expression: "pub type PokemonType {\n  Air\n  Water\n  Fire\n}\n        \n  pub fn f(pokemon_type: PokemonType) {\n    case pokemon_type {\n      Water | Air -> soak()\n      Fire -> burn()\n    }\n  }"
+expression: "pub type PokemonType {\n  Air\n  Water\n  Fire\n}\n\n  pub fn f(pokemon_type: PokemonType) {\n    case pokemon_type {\n      Water | Air -> soak()\n      Fire -> burn()\n    }\n  }"
 ---
 ----- BEFORE ACTION
 pub type PokemonType {
@@ -8,7 +8,7 @@ pub type PokemonType {
   Water
   Fire
 }
-        
+
   pub fn f(pokemon_type: PokemonType) {
     case pokemon_type {
       Water | Air -> soak()
@@ -24,7 +24,7 @@ pub type PokemonType {
   Water
   Fire
 }
-        
+
   pub fn f(pokemon_type: PokemonType) {
     case pokemon_type {
       Water | Air -> {