From a2685ba17bb206d23b31e45702a6f0aef3f6d11d Mon Sep 17 00:00:00 2001 From: GearsDatapacks Date: Mon, 23 Dec 2024 11:50:00 +0000 Subject: [PATCH 01/10] Implement code action to generate dynamic decoder --- compiler-core/src/ast.rs | 1 + compiler-core/src/ast/visit.rs | 18 ++- .../src/language_server/code_action.rs | 148 +++++++++++++++++- compiler-core/src/language_server/engine.rs | 5 +- .../src/language_server/tests/action.rs | 19 +++ ...sts__action__generate_dynamic_decoder.snap | 31 ++++ compiler-core/src/type_/printer.rs | 8 + 7 files changed, 223 insertions(+), 7 deletions(-) create mode 100644 compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder.snap diff --git a/compiler-core/src/ast.rs b/compiler-core/src/ast.rs index 9a2d3ea246d..58321380fea 100644 --- a/compiler-core/src/ast.rs +++ b/compiler-core/src/ast.rs @@ -729,6 +729,7 @@ pub struct ModuleConstant { } pub type UntypedCustomType = CustomType<()>; +pub type TypedCustomType = CustomType>; #[derive(Debug, Clone, PartialEq, Eq)] /// A newly defined type with one or more constructors. diff --git a/compiler-core/src/ast/visit.rs b/compiler-core/src/ast/visit.rs index 3df1b1c0dd9..9d3b4c34580 100644 --- a/compiler-core/src/ast/visit.rs +++ b/compiler-core/src/ast/visit.rs @@ -53,9 +53,9 @@ use crate::type_::Type; use super::{ untyped::FunctionLiteralKind, AssignName, BinOp, BitArrayOption, CallArg, Definition, Pattern, - SrcSpan, Statement, TodoKind, TypeAst, TypedArg, TypedAssignment, TypedClause, TypedDefinition, - TypedExpr, TypedExprBitArraySegment, TypedFunction, TypedModule, TypedModuleConstant, - TypedPattern, TypedPatternBitArraySegment, TypedStatement, TypedUse, + SrcSpan, Statement, TodoKind, TypeAst, TypedArg, TypedAssignment, TypedClause, TypedCustomType, + TypedDefinition, TypedExpr, TypedExprBitArraySegment, TypedFunction, TypedModule, + TypedModuleConstant, TypedPattern, TypedPatternBitArraySegment, TypedStatement, TypedUse, }; pub trait Visit<'ast> { @@ -75,6 +75,10 @@ pub trait Visit<'ast> { visit_typed_module_constant(self, constant); } + fn visit_typed_custom_type(&mut self, custom_type: &'ast TypedCustomType) { + visit_typed_custom_type(self, custom_type); + } + fn visit_typed_expr(&mut self, expr: &'ast TypedExpr) { visit_typed_expr(self, expr); } @@ -493,7 +497,7 @@ where match def { Definition::Function(fun) => v.visit_typed_function(fun), Definition::TypeAlias(_typealias) => { /* TODO */ } - Definition::CustomType(_custom_type) => { /* TODO */ } + Definition::CustomType(custom_type) => v.visit_typed_custom_type(custom_type), Definition::Import(_import) => { /* TODO */ } Definition::ModuleConstant(constant) => v.visit_typed_module_constant(constant), } @@ -596,6 +600,12 @@ where { } +pub fn visit_typed_custom_type<'a, V>(_v: &mut V, _custom_type: &'a TypedCustomType) +where + V: Visit<'a> + ?Sized, +{ +} + pub fn visit_typed_expr<'a, V>(v: &mut V, node: &'a TypedExpr) where V: Visit<'a> + ?Sized, diff --git a/compiler-core/src/language_server/code_action.rs b/compiler-core/src/language_server/code_action.rs index b8bc6aa04e5..f958aeebe51 100644 --- a/compiler-core/src/language_server/code_action.rs +++ b/compiler-core/src/language_server/code_action.rs @@ -17,9 +17,10 @@ use crate::{ printer::Printer, FieldMap, ModuleValueConstructor, Type, TypedCallArg, }, - Error, + Error, STDLIB_PACKAGE_NAME, }; use ecow::{eco_format, EcoString}; +use heck::ToSnakeCase; use im::HashMap; use itertools::Itertools; use lsp_types::{CodeAction, CodeActionKind, CodeActionParams, Position, Range, TextEdit, Url}; @@ -2875,3 +2876,148 @@ impl<'ast> ast::visit::Visit<'ast> for VariablesNames { let _ = self.names.insert(name.clone()); } } + +/// Builder for code action to apply the "generate dynamic decoder action. +/// +pub struct GenerateDynamicDecoder<'a> { + module: &'a Module, + params: &'a CodeActionParams, + edits: TextEdits<'a>, + printer: Printer<'a>, + actions: &'a mut Vec, +} + +const DECODE_MODULE: &str = "gleam/dynamic/decode"; + +impl<'a> GenerateDynamicDecoder<'a> { + pub fn new( + module: &'a Module, + line_numbers: &'a LineNumbers, + params: &'a CodeActionParams, + actions: &'a mut Vec, + ) -> Self { + let printer = Printer::new(&module.ast.names); + Self { + module, + params, + edits: TextEdits::new(line_numbers), + printer, + actions, + } + } + + pub fn code_actions(&mut self) { + self.visit_typed_module(&self.module.ast); + } + + fn decoder_for(&mut self, type_: &Type) -> EcoString { + let module_name = self.printer.print_module(DECODE_MODULE); + // TODO: List, Dict and Option + if type_.is_bit_array() { + eco_format!("{module_name}.bit_array") + } else if type_.is_bool() { + eco_format!("{module_name}.bool") + } else if type_.is_float() { + eco_format!("{module_name}.float") + } else if type_.is_int() { + eco_format!("{module_name}.int") + } else if type_.is_string() { + eco_format!("{module_name}.string") + } else if type_.named_type_name() == Some(("gleam/dynamic".into(), "Dynamic".into())) { + eco_format!("{module_name}.dynamic") + } else { + eco_format!( + r#"todo as "Decoder for {}""#, + self.printer.print_type(type_) + ) + } + } + + fn decode_field(&mut self, field: &RecordField<'_>) -> EcoString { + let decoder = self.decoder_for(field.type_); + + eco_format!( + r#"use {name} <- {module}.field("{name}", {decoder})"#, + name = field.label, + module = self.printer.print_module(DECODE_MODULE) + ) + } +} + +struct RecordField<'a> { + label: &'a str, + type_: &'a Type, +} + +impl<'ast> ast::visit::Visit<'ast> for GenerateDynamicDecoder<'ast> { + fn visit_typed_custom_type(&mut self, custom_type: &'ast ast::TypedCustomType) { + let range = self.edits.src_span_to_lsp_range(custom_type.location); + if !overlaps(self.params.range, range) { + return; + } + + // For now, we only generate dynamic decoders for types with one variant. + let constructor = match custom_type.constructors.as_slice() { + [constructor] => constructor, + _ => return, + }; + + let name = eco_format!("{}_decoder", custom_type.name.to_snake_case()); + + let Some(fields): Option> = constructor + .arguments + .iter() + .map(|argument| { + Some(RecordField { + label: argument.label.as_ref().map(|(_, name)| name.as_str())?, + type_: &argument.type_, + }) + }) + .collect() + else { + return; + }; + + let field_decoders: Vec<_> = fields + .iter() + .map(|field| self.decode_field(field)) + .collect(); + + let decoder_type = self.printer.print_type(&Type::Named { + publicity: ast::Publicity::Public, + package: STDLIB_PACKAGE_NAME.into(), + module: DECODE_MODULE.into(), + name: "Decoder".into(), + args: vec![], + inferred_variant: None, + }); + let decode_module = self.printer.print_module(DECODE_MODULE); + + let mut field_names = fields.iter().map(|field| field.label); + + let function = format!( + " + +fn {name}() -> {decoder_type}({type_name}) {{ + {decoders} + + {decode_module}.success({constructor_name}({fields}:)) +}}", + decoders = field_decoders.join("\n "), + type_name = custom_type.name, + constructor_name = constructor.name, + fields = field_names.join(":, ") + ); + + self.edits.insert(custom_type.end_position, function); + + CodeActionBuilder::new("Generate dynamic decoder") + .kind(CodeActionKind::REFACTOR) + .preferred(false) + .changes( + self.params.text_document.uri.clone(), + std::mem::take(&mut self.edits.edits), + ) + .push_to(self.actions); + } +} diff --git a/compiler-core/src/language_server/engine.rs b/compiler-core/src/language_server/engine.rs index 00598af3c7f..16334ab67ae 100644 --- a/compiler-core/src/language_server/engine.rs +++ b/compiler-core/src/language_server/engine.rs @@ -33,8 +33,8 @@ use super::{ code_action_add_missing_patterns, code_action_convert_qualified_constructor_to_unqualified, code_action_convert_unqualified_constructor_to_qualified, code_action_import_module, code_action_inexhaustive_let_to_case, AddAnnotations, CodeActionBuilder, DesugarUse, - ExpandFunctionCapture, ExtractVariable, FillInMissingLabelledArgs, LabelShorthandSyntax, - LetAssertToCase, RedundantTupleInCaseSubject, TurnIntoUse, + ExpandFunctionCapture, ExtractVariable, FillInMissingLabelledArgs, GenerateDynamicDecoder, + LabelShorthandSyntax, LetAssertToCase, RedundantTupleInCaseSubject, TurnIntoUse, }, completer::Completer, signature_help, src_span_to_lsp_range, DownloadDependencies, MakeLocker, @@ -334,6 +334,7 @@ where actions.extend(TurnIntoUse::new(module, &lines, ¶ms).code_actions()); actions.extend(ExpandFunctionCapture::new(module, &lines, ¶ms).code_actions()); actions.extend(ExtractVariable::new(module, &lines, ¶ms).code_actions()); + GenerateDynamicDecoder::new(module, &lines, ¶ms, &mut actions).code_actions(); AddAnnotations::new(module, &lines, ¶ms).code_action(&mut actions); Ok(if actions.is_empty() { None diff --git a/compiler-core/src/language_server/tests/action.rs b/compiler-core/src/language_server/tests/action.rs index b5fd5f464ac..948e35cf5c1 100644 --- a/compiler-core/src/language_server/tests/action.rs +++ b/compiler-core/src/language_server/tests/action.rs @@ -70,6 +70,7 @@ const DESUGAR_USE_EXPRESSION: &str = "Convert from `use`"; const CONVERT_TO_USE: &str = "Convert to `use`"; const EXTRACT_VARIABLE: &str = "Extract variable"; const EXPAND_FUNCTION_CAPTURE: &str = "Expand function capture"; +const GENERATE_DYNAMIC_DECODER: &str = "Generate dynamic decoder"; macro_rules! assert_code_action { ($title:expr, $code:literal, $range:expr $(,)?) => { @@ -4034,3 +4035,21 @@ fn expand_function_capture_does_not_shadow_variables() { find_position_of("wibble").to_selection() ); } + +#[test] +fn generate_dynamic_decoder() { + let src = " +import gleam/option + +pub type Wibble(value) { + Wibble(counter: Int, values: List(value), next: Wibble(value), message: option.Option(String)) +} + "; + + assert_code_action!( + GENERATE_DYNAMIC_DECODER, + TestProject::for_source(src) + .add_module("gleam/option", "pub type Option(a) { Some(a) None }"), + find_position_of("type").to_selection() + ); +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder.snap new file mode 100644 index 00000000000..45b829df36e --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder.snap @@ -0,0 +1,31 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "\nimport gleam/option\n\npub type Wibble(value) {\n Wibble(counter: Int, values: List(value), next: Wibble(value), message: option.Option(String))\n}\n " +--- +----- BEFORE ACTION + +import gleam/option + +pub type Wibble(value) { + ↑ + Wibble(counter: Int, values: List(value), next: Wibble(value), message: option.Option(String)) +} + + + +----- AFTER ACTION + +import gleam/option + +pub type Wibble(value) { + Wibble(counter: Int, values: List(value), next: Wibble(value), message: option.Option(String)) +} + +fn wibble_decoder() -> decode.Decoder(Wibble) { + use counter <- decode.field("counter", decode.int) + use values <- decode.field("values", todo as "Decoder for List(value)") + use next <- decode.field("next", todo as "Decoder for Wibble(value)") + use message <- decode.field("message", todo as "Decoder for option.Option(String)") + + decode.success(Wibble(counter:, values:, next:, message:)) +} diff --git a/compiler-core/src/type_/printer.rs b/compiler-core/src/type_/printer.rs index e306e691606..ddce697f80c 100644 --- a/compiler-core/src/type_/printer.rs +++ b/compiler-core/src/type_/printer.rs @@ -318,6 +318,14 @@ impl<'a> Printer<'a> { buffer } + pub fn print_module(&self, module: &str) -> EcoString { + if let Some(module) = self.names.imported_modules.get(module) { + module.clone() + } else { + module.split("/").last().unwrap_or(module).into() + } + } + pub fn print_type_without_aliases(&mut self, type_: &Type) -> EcoString { let mut buffer = EcoString::new(); self.print(type_, &mut buffer, PrintMode::ExpandAliases); From e25bf6e122482177aae75253d1aae137a3b4d1a5 Mon Sep 17 00:00:00 2001 From: GearsDatapacks Date: Mon, 23 Dec 2024 12:05:23 +0000 Subject: [PATCH 02/10] Import decode module when running generate decoder action --- .../src/language_server/code_action.rs | 23 +++++++++++++++++++ compiler-core/src/language_server/edits.rs | 2 +- ...sts__action__generate_dynamic_decoder.snap | 1 + compiler-core/src/type_/printer.rs | 4 ++++ 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/compiler-core/src/language_server/code_action.rs b/compiler-core/src/language_server/code_action.rs index f958aeebe51..257b3afe248 100644 --- a/compiler-core/src/language_server/code_action.rs +++ b/compiler-core/src/language_server/code_action.rs @@ -3010,6 +3010,7 @@ fn {name}() -> {decoder_type}({type_name}) {{ ); self.edits.insert(custom_type.end_position, function); + maybe_import(&mut self.edits, self.module, DECODE_MODULE); CodeActionBuilder::new("Generate dynamic decoder") .kind(CodeActionKind::REFACTOR) @@ -3021,3 +3022,25 @@ fn {name}() -> {decoder_type}({type_name}) {{ .push_to(self.actions); } } + +fn maybe_import(edits: &mut TextEdits<'_>, module: &Module, module_name: &str) { + if module.ast.names.is_imported(module_name) { + return; + } + + let first_import_pos = position_of_first_definition_if_import(module, edits.line_numbers); + let first_is_import = first_import_pos.is_some(); + let import_location = first_import_pos.unwrap_or_default(); + let after_import_newlines = add_newlines_after_import( + import_location, + first_is_import, + edits.line_numbers, + &module.code, + ); + + edits.edits.push(get_import_edit( + import_location, + module_name, + &after_import_newlines, + )); +} diff --git a/compiler-core/src/language_server/edits.rs b/compiler-core/src/language_server/edits.rs index b0ce6c2fec7..8157169cb7c 100644 --- a/compiler-core/src/language_server/edits.rs +++ b/compiler-core/src/language_server/edits.rs @@ -61,7 +61,7 @@ pub fn add_newlines_after_import( pub fn get_import_edit( import_location: Position, - module_full_name: &EcoString, + module_full_name: &str, insert_newlines: &Newlines, ) -> TextEdit { let new_lines = match insert_newlines { diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder.snap index 45b829df36e..bddbdb5e39b 100644 --- a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder.snap +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder.snap @@ -15,6 +15,7 @@ pub type Wibble(value) { ----- AFTER ACTION +import gleam/dynamic/decode import gleam/option pub type Wibble(value) { diff --git a/compiler-core/src/type_/printer.rs b/compiler-core/src/type_/printer.rs index ddce697f80c..4fd0fa96d45 100644 --- a/compiler-core/src/type_/printer.rs +++ b/compiler-core/src/type_/printer.rs @@ -246,6 +246,10 @@ impl Names { NameContextInformation::Unimported(name.as_str()) } + + pub fn is_imported(&self, module: &str) -> bool { + self.imported_modules.contains_key(module) + } } #[derive(Debug)] From e332099f0015455088df00bfeadfc87e18fbf180 Mon Sep 17 00:00:00 2001 From: GearsDatapacks Date: Mon, 23 Dec 2024 15:17:32 +0000 Subject: [PATCH 03/10] Add support for generating decoders for types with parameters --- .../src/language_server/code_action.rs | 30 +++++++++++++++---- compiler-core/src/type_.rs | 17 +++++++++++ 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/compiler-core/src/language_server/code_action.rs b/compiler-core/src/language_server/code_action.rs index 257b3afe248..715fd1b3574 100644 --- a/compiler-core/src/language_server/code_action.rs +++ b/compiler-core/src/language_server/code_action.rs @@ -2912,7 +2912,6 @@ impl<'a> GenerateDynamicDecoder<'a> { fn decoder_for(&mut self, type_: &Type) -> EcoString { let module_name = self.printer.print_module(DECODE_MODULE); - // TODO: List, Dict and Option if type_.is_bit_array() { eco_format!("{module_name}.bit_array") } else if type_.is_bool() { @@ -2926,10 +2925,31 @@ impl<'a> GenerateDynamicDecoder<'a> { } else if type_.named_type_name() == Some(("gleam/dynamic".into(), "Dynamic".into())) { eco_format!("{module_name}.dynamic") } else { - eco_format!( - r#"todo as "Decoder for {}""#, - self.printer.print_type(type_) - ) + let type_information = type_.named_type_information(); + let type_information = type_information.as_ref().map(|(module, name, arguments)| { + (module.as_str(), name.as_str(), arguments.as_slice()) + }); + + match type_information { + Some(("gleam/dynamic", "Dynamic", _)) => eco_format!("{module_name}.dynamic"), + Some(("gleam", "List", [element])) => { + eco_format!("{module_name}.list({})", self.decoder_for(element)) + } + Some(("gleam/option", "Option", [some])) => { + eco_format!("{module_name}.optional({})", self.decoder_for(some)) + } + Some(("gleam/dict", "Dict", [key, value])) => { + eco_format!( + "{module_name}.dict({}, {})", + self.decoder_for(key), + self.decoder_for(value) + ) + } + Some(_) | None => eco_format!( + r#"todo as "Decoder for {}""#, + self.printer.print_type(type_) + ), + } } } diff --git a/compiler-core/src/type_.rs b/compiler-core/src/type_.rs index 0aa5beab6e1..eb700ee61c0 100644 --- a/compiler-core/src/type_.rs +++ b/compiler-core/src/type_.rs @@ -264,6 +264,16 @@ impl Type { } } + pub fn named_type_information(&self) -> Option<(EcoString, EcoString, Vec>)> { + match self { + Self::Named { + module, name, args, .. + } => Some((module.clone(), name.clone(), args.clone())), + Self::Var { type_ } => type_.borrow().named_type_information(), + _ => None, + } + } + pub fn set_custom_type_variant(&mut self, index: u16) { match self { Type::Named { @@ -1009,6 +1019,13 @@ impl TypeVar { } } + pub fn named_type_information(&self) -> Option<(EcoString, EcoString, Vec>)> { + match self { + Self::Link { type_ } => type_.named_type_information(), + Self::Unbound { .. } | Self::Generic { .. } => None, + } + } + pub fn set_custom_type_variant(&mut self, index: u16) { match self { Self::Link { type_ } => Arc::make_mut(type_).set_custom_type_variant(index), From a5c20159142d9f07b170675279ec864dfd2e22ed Mon Sep 17 00:00:00 2001 From: GearsDatapacks Date: Mon, 23 Dec 2024 15:34:30 +0000 Subject: [PATCH 04/10] Add support for generic types when generating decoders --- compiler-core/src/language_server/code_action.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/compiler-core/src/language_server/code_action.rs b/compiler-core/src/language_server/code_action.rs index 715fd1b3574..37ce7a6a0d6 100644 --- a/compiler-core/src/language_server/code_action.rs +++ b/compiler-core/src/language_server/code_action.rs @@ -2945,7 +2945,7 @@ impl<'a> GenerateDynamicDecoder<'a> { self.decoder_for(value) ) } - Some(_) | None => eco_format!( + _ => eco_format!( r#"todo as "Decoder for {}""#, self.printer.print_type(type_) ), @@ -3014,11 +3014,22 @@ impl<'ast> ast::visit::Visit<'ast> for GenerateDynamicDecoder<'ast> { let decode_module = self.printer.print_module(DECODE_MODULE); let mut field_names = fields.iter().map(|field| field.label); + let parameters = match custom_type.parameters.len() { + 0 => EcoString::new(), + _ => eco_format!( + "({})", + custom_type + .parameters + .iter() + .map(|(_, name)| name) + .join(", ") + ), + }; let function = format!( " -fn {name}() -> {decoder_type}({type_name}) {{ +fn {name}() -> {decoder_type}({type_name}{parameters}) {{ {decoders} {decode_module}.success({constructor_name}({fields}:)) From 2ec24750bf858b73b8d36abdc0d90aaf062bd94a Mon Sep 17 00:00:00 2001 From: GearsDatapacks Date: Mon, 23 Dec 2024 15:51:06 +0000 Subject: [PATCH 05/10] Add tests --- .../src/language_server/code_action.rs | 2 - .../src/language_server/tests/action.rs | 73 ++++++++++++++++++- ...sts__action__generate_dynamic_decoder.snap | 30 ++++---- ...namic_decoder_already_imported_module.snap | 29 ++++++++ ...enerate_dynamic_decoder_complex_types.snap | 46 ++++++++++++ 5 files changed, 158 insertions(+), 22 deletions(-) create mode 100644 compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder_already_imported_module.snap create mode 100644 compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder_complex_types.snap diff --git a/compiler-core/src/language_server/code_action.rs b/compiler-core/src/language_server/code_action.rs index 37ce7a6a0d6..30b0ad6c70d 100644 --- a/compiler-core/src/language_server/code_action.rs +++ b/compiler-core/src/language_server/code_action.rs @@ -2922,8 +2922,6 @@ impl<'a> GenerateDynamicDecoder<'a> { eco_format!("{module_name}.int") } else if type_.is_string() { eco_format!("{module_name}.string") - } else if type_.named_type_name() == Some(("gleam/dynamic".into(), "Dynamic".into())) { - eco_format!("{module_name}.dynamic") } else { let type_information = type_.named_type_information(); let type_information = type_information.as_ref().map(|(module, name, arguments)| { diff --git a/compiler-core/src/language_server/tests/action.rs b/compiler-core/src/language_server/tests/action.rs index 948e35cf5c1..5d08acb4b00 100644 --- a/compiler-core/src/language_server/tests/action.rs +++ b/compiler-core/src/language_server/tests/action.rs @@ -4038,18 +4038,85 @@ fn expand_function_capture_does_not_shadow_variables() { #[test] fn generate_dynamic_decoder() { + assert_code_action!( + GENERATE_DYNAMIC_DECODER, + " +pub type Person { + Person(name: String, age: Int, height: Float, is_cool: Bool, brain: BitArray) +} +", + find_position_of("type").to_selection() + ); +} + +#[test] +fn generate_dynamic_decoder_complex_types() { let src = " import gleam/option +import gleam/dynamic +import gleam/dict + +pub type Something pub type Wibble(value) { - Wibble(counter: Int, values: List(value), next: Wibble(value), message: option.Option(String)) + Wibble( + maybe: option.Option(Something), + map: dict.Dict(String, List(value)), + unknown: List(dynamic.Dynamic), + ) } - "; +"; assert_code_action!( GENERATE_DYNAMIC_DECODER, TestProject::for_source(src) - .add_module("gleam/option", "pub type Option(a) { Some(a) None }"), + .add_module("gleam/option", "pub type Option(a)") + .add_module("gleam/dynamic", "pub type Dynamic") + .add_module("gleam/dict", "pub type Dict(k, v)"), + find_position_of("type W").to_selection() + ); +} + +#[test] +fn generate_dynamic_decoder_already_imported_module() { + let src = " +import gleam/dynamic/decode as dyn_dec + +pub type Wibble { + Wibble(a: Int, b: Float, c: String) +} +"; + + assert_code_action!( + GENERATE_DYNAMIC_DECODER, + TestProject::for_source(src).add_module("gleam/dynamic/decode", "pub type Decoder(a)"), + find_position_of("type W").to_selection() + ); +} + +#[test] +fn no_code_action_to_generate_dynamic_decoder_for_multi_variant_type() { + assert_no_code_actions!( + GENERATE_DYNAMIC_DECODER, + " +pub type Wibble { + Wibble(wibble: Int) + Wobble(wobble: Float) +} +", + find_position_of("type").to_selection() + ); +} + +#[test] +fn no_code_action_to_generate_dynamic_decoder_for_type_without_labels() { + assert_no_code_actions!( + GENERATE_DYNAMIC_DECODER, + " +pub type Wibble { + Wibble(Int, Int, String) +} +", find_position_of("type").to_selection() ); } diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder.snap index bddbdb5e39b..8c5c24faba6 100644 --- a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder.snap +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder.snap @@ -1,32 +1,28 @@ --- source: compiler-core/src/language_server/tests/action.rs -expression: "\nimport gleam/option\n\npub type Wibble(value) {\n Wibble(counter: Int, values: List(value), next: Wibble(value), message: option.Option(String))\n}\n " +expression: "\npub type Person {\n Person(name: String, age: Int, height: Float, is_cool: Bool, brain: BitArray)\n}\n" --- ----- BEFORE ACTION -import gleam/option - -pub type Wibble(value) { - ↑ - Wibble(counter: Int, values: List(value), next: Wibble(value), message: option.Option(String)) +pub type Person { + ↑ + Person(name: String, age: Int, height: Float, is_cool: Bool, brain: BitArray) } - ----- AFTER ACTION - import gleam/dynamic/decode -import gleam/option -pub type Wibble(value) { - Wibble(counter: Int, values: List(value), next: Wibble(value), message: option.Option(String)) +pub type Person { + Person(name: String, age: Int, height: Float, is_cool: Bool, brain: BitArray) } -fn wibble_decoder() -> decode.Decoder(Wibble) { - use counter <- decode.field("counter", decode.int) - use values <- decode.field("values", todo as "Decoder for List(value)") - use next <- decode.field("next", todo as "Decoder for Wibble(value)") - use message <- decode.field("message", todo as "Decoder for option.Option(String)") +fn person_decoder() -> decode.Decoder(Person) { + use name <- decode.field("name", decode.string) + use age <- decode.field("age", decode.int) + use height <- decode.field("height", decode.float) + use is_cool <- decode.field("is_cool", decode.bool) + use brain <- decode.field("brain", decode.bit_array) - decode.success(Wibble(counter:, values:, next:, message:)) + decode.success(Person(name:, age:, height:, is_cool:, brain:)) } diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder_already_imported_module.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder_already_imported_module.snap new file mode 100644 index 00000000000..3fcf99dcc92 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder_already_imported_module.snap @@ -0,0 +1,29 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "\nimport gleam/dynamic/decode as dyn_dec\n\npub type Wibble {\n Wibble(a: Int, b: Float, c: String)\n}\n" +--- +----- BEFORE ACTION + +import gleam/dynamic/decode as dyn_dec + +pub type Wibble { + ↑ + Wibble(a: Int, b: Float, c: String) +} + + +----- AFTER ACTION + +import gleam/dynamic/decode as dyn_dec + +pub type Wibble { + Wibble(a: Int, b: Float, c: String) +} + +fn wibble_decoder() -> dyn_dec.Decoder(Wibble) { + use a <- dyn_dec.field("a", dyn_dec.int) + use b <- dyn_dec.field("b", dyn_dec.float) + use c <- dyn_dec.field("c", dyn_dec.string) + + dyn_dec.success(Wibble(a:, b:, c:)) +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder_complex_types.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder_complex_types.snap new file mode 100644 index 00000000000..5f00ed260e0 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder_complex_types.snap @@ -0,0 +1,46 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "\nimport gleam/option\nimport gleam/dynamic\nimport gleam/dict\n\npub type Something\n\npub type Wibble(value) {\n Wibble(\n maybe: option.Option(Something),\n map: dict.Dict(String, List(value)),\n unknown: List(dynamic.Dynamic),\n )\n}\n" +--- +----- BEFORE ACTION + +import gleam/option +import gleam/dynamic +import gleam/dict + +pub type Something + +pub type Wibble(value) { + ↑ + Wibble( + maybe: option.Option(Something), + map: dict.Dict(String, List(value)), + unknown: List(dynamic.Dynamic), + ) +} + + +----- AFTER ACTION + +import gleam/dynamic/decode +import gleam/option +import gleam/dynamic +import gleam/dict + +pub type Something + +pub type Wibble(value) { + Wibble( + maybe: option.Option(Something), + map: dict.Dict(String, List(value)), + unknown: List(dynamic.Dynamic), + ) +} + +fn wibble_decoder() -> decode.Decoder(Wibble(value)) { + use maybe <- decode.field("maybe", decode.optional(todo as "Decoder for Something")) + use map <- decode.field("map", decode.dict(decode.string, decode.list(todo as "Decoder for value"))) + use unknown <- decode.field("unknown", decode.list(decode.dynamic)) + + decode.success(Wibble(maybe:, map:, unknown:)) +} From abd5c2731e7247c7fb8ae06a46060cb57cb8ead0 Mon Sep 17 00:00:00 2001 From: GearsDatapacks Date: Mon, 23 Dec 2024 15:55:21 +0000 Subject: [PATCH 06/10] Changelog --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3544989741..4be83b7217a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -208,6 +208,34 @@ ([Giacomo Cavalieri](https://github.com/giacomocavalieri)) +- The language server now suggests a code action to generate a dynamic decoder + for a custom type. For example, this code: + + ```gleam + pub type Person { + Person(name: String, age: Int) + } + ``` + + Will become: + + ```gleam + import gleam/dynamic/decode + + pub type Person { + Person(name: String, age: Int) + } + + fn person_decoder() -> decode.Decoder(Person) { + use name <- decode.field("name", decode.string) + use age <- decode.field("age", decode.int) + + decode.success(Person(name:, age:)) + } + ``` + + ([Surya Rose](https://github.com/GearsDatapacks)) + ### Formatter - The formatter now adds a `todo` inside empty blocks. From 8270d4d0160f7bae75225e002724ddf2bcaa3d80 Mon Sep 17 00:00:00 2001 From: GearsDatapacks Date: Thu, 26 Dec 2024 10:22:35 +0000 Subject: [PATCH 07/10] Add support for decoding tuples --- .../src/language_server/code_action.rs | 207 ++++++++++++------ compiler-core/src/type_.rs | 8 + 2 files changed, 151 insertions(+), 64 deletions(-) diff --git a/compiler-core/src/language_server/code_action.rs b/compiler-core/src/language_server/code_action.rs index 30b0ad6c70d..5fd46b59205 100644 --- a/compiler-core/src/language_server/code_action.rs +++ b/compiler-core/src/language_server/code_action.rs @@ -14,7 +14,7 @@ use crate::{ type_::{ self, error::{ModuleSuggestion, VariableOrigin}, - printer::Printer, + printer::{Names, Printer}, FieldMap, ModuleValueConstructor, Type, TypedCallArg, }, Error, STDLIB_PACKAGE_NAME, @@ -2909,62 +2909,6 @@ impl<'a> GenerateDynamicDecoder<'a> { pub fn code_actions(&mut self) { self.visit_typed_module(&self.module.ast); } - - fn decoder_for(&mut self, type_: &Type) -> EcoString { - let module_name = self.printer.print_module(DECODE_MODULE); - if type_.is_bit_array() { - eco_format!("{module_name}.bit_array") - } else if type_.is_bool() { - eco_format!("{module_name}.bool") - } else if type_.is_float() { - eco_format!("{module_name}.float") - } else if type_.is_int() { - eco_format!("{module_name}.int") - } else if type_.is_string() { - eco_format!("{module_name}.string") - } else { - let type_information = type_.named_type_information(); - let type_information = type_information.as_ref().map(|(module, name, arguments)| { - (module.as_str(), name.as_str(), arguments.as_slice()) - }); - - match type_information { - Some(("gleam/dynamic", "Dynamic", _)) => eco_format!("{module_name}.dynamic"), - Some(("gleam", "List", [element])) => { - eco_format!("{module_name}.list({})", self.decoder_for(element)) - } - Some(("gleam/option", "Option", [some])) => { - eco_format!("{module_name}.optional({})", self.decoder_for(some)) - } - Some(("gleam/dict", "Dict", [key, value])) => { - eco_format!( - "{module_name}.dict({}, {})", - self.decoder_for(key), - self.decoder_for(value) - ) - } - _ => eco_format!( - r#"todo as "Decoder for {}""#, - self.printer.print_type(type_) - ), - } - } - } - - fn decode_field(&mut self, field: &RecordField<'_>) -> EcoString { - let decoder = self.decoder_for(field.type_); - - eco_format!( - r#"use {name} <- {module}.field("{name}", {decoder})"#, - name = field.label, - module = self.printer.print_module(DECODE_MODULE) - ) - } -} - -struct RecordField<'a> { - label: &'a str, - type_: &'a Type, } impl<'ast> ast::visit::Visit<'ast> for GenerateDynamicDecoder<'ast> { @@ -2987,7 +2931,9 @@ impl<'ast> ast::visit::Visit<'ast> for GenerateDynamicDecoder<'ast> { .iter() .map(|argument| { Some(RecordField { - label: argument.label.as_ref().map(|(_, name)| name.as_str())?, + label: RecordLabel::Labeled( + argument.label.as_ref().map(|(_, name)| name.as_str())?, + ), type_: &argument.type_, }) }) @@ -2996,10 +2942,12 @@ impl<'ast> ast::visit::Visit<'ast> for GenerateDynamicDecoder<'ast> { return; }; - let field_decoders: Vec<_> = fields + let mut decoder_printer = DecoderPrinter::new(&self.module.ast.names); + + let decoders = fields .iter() - .map(|field| self.decode_field(field)) - .collect(); + .map(|field| decoder_printer.decode_field(field, 2)) + .join("\n"); let decoder_type = self.printer.print_type(&Type::Named { publicity: ast::Publicity::Public, @@ -3011,7 +2959,7 @@ impl<'ast> ast::visit::Visit<'ast> for GenerateDynamicDecoder<'ast> { }); let decode_module = self.printer.print_module(DECODE_MODULE); - let mut field_names = fields.iter().map(|field| field.label); + let mut field_names = fields.iter().map(|field| field.label.variable_name()); let parameters = match custom_type.parameters.len() { 0 => EcoString::new(), _ => eco_format!( @@ -3028,11 +2976,10 @@ impl<'ast> ast::visit::Visit<'ast> for GenerateDynamicDecoder<'ast> { " fn {name}() -> {decoder_type}({type_name}{parameters}) {{ - {decoders} +{decoders} {decode_module}.success({constructor_name}({fields}:)) }}", - decoders = field_decoders.join("\n "), type_name = custom_type.name, constructor_name = constructor.name, fields = field_names.join(":, ") @@ -3073,3 +3020,135 @@ fn maybe_import(edits: &mut TextEdits<'_>, module: &Module, module_name: &str) { &after_import_newlines, )); } + +struct DecoderPrinter<'a> { + printer: Printer<'a>, +} + +struct RecordField<'a> { + label: RecordLabel<'a>, + type_: &'a Type, +} + +enum RecordLabel<'a> { + Labeled(&'a str), + Unlabeled(usize), +} + +impl<'a> RecordLabel<'a> { + fn field_key(&self) -> EcoString { + match self { + RecordLabel::Labeled(label) => eco_format!("\"{label}\""), + RecordLabel::Unlabeled(index) => { + eco_format!("{index}") + } + } + } + + fn variable_name(&self) -> EcoString { + match self { + RecordLabel::Labeled(label) => (*label).into(), + RecordLabel::Unlabeled(mut index) => { + let mut name = EcoString::new(); + let alphabet_length = 26; + let alphabet_offset = 'a' as u8; + loop { + let alphabet_index = (index % alphabet_length) as u8; + name.push((alphabet_offset + alphabet_index) as char); + index /= alphabet_length; + + if index == 0 { + break; + } + index -= 1; + } + name + } + } + } +} + +impl<'a> DecoderPrinter<'a> { + fn new(names: &'a Names) -> Self { + Self { + printer: Printer::new(names), + } + } + + fn decoder_for(&mut self, type_: &Type, indent: usize) -> EcoString { + let module_name = self.printer.print_module(DECODE_MODULE); + if type_.is_bit_array() { + eco_format!("{module_name}.bit_array") + } else if type_.is_bool() { + eco_format!("{module_name}.bool") + } else if type_.is_float() { + eco_format!("{module_name}.float") + } else if type_.is_int() { + eco_format!("{module_name}.int") + } else if type_.is_string() { + eco_format!("{module_name}.string") + } else if let Some(types) = type_.tuple_types() { + let fields = types + .iter() + .enumerate() + .map(|(index, type_)| RecordField { + type_, + label: RecordLabel::Unlabeled(index), + }) + .collect_vec(); + let decoders = fields + .iter() + .map(|field| self.decode_field(field, indent + 2)) + .join("\n"); + let mut field_names = fields.iter().map(|field| field.label.variable_name()); + + eco_format!( + "{{ +{decoders} + +{indent} {module_name}.success(#({fields})) +{indent}}}", + fields = field_names.join(", "), + indent = " ".repeat(indent) + ) + } else { + let type_information = type_.named_type_information(); + let type_information = type_information.as_ref().map(|(module, name, arguments)| { + (module.as_str(), name.as_str(), arguments.as_slice()) + }); + + match type_information { + Some(("gleam/dynamic", "Dynamic", _)) => eco_format!("{module_name}.dynamic"), + Some(("gleam", "List", [element])) => { + eco_format!("{module_name}.list({})", self.decoder_for(element, indent)) + } + Some(("gleam/option", "Option", [some])) => { + eco_format!("{module_name}.optional({})", self.decoder_for(some, indent)) + } + Some(("gleam/dict", "Dict", [key, value])) => { + eco_format!( + "{module_name}.dict({}, {})", + self.decoder_for(key, indent), + self.decoder_for(value, indent) + ) + } + _ => eco_format!( + r#"todo as "Decoder for {}""#, + self.printer.print_type(type_) + ), + } + } + } + + fn decode_field(&mut self, field: &RecordField<'_>, indent: usize) -> EcoString { + let decoder = self.decoder_for(field.type_, indent); + + eco_format!( + r#"{indent}use {variable} <- {module}.field({field}, {decoder})"#, + indent = " ".repeat(indent), + variable = field.label.variable_name(), + field = field.label.field_key(), + module = self.printer.print_module(DECODE_MODULE) + ) + } +} diff --git a/compiler-core/src/type_.rs b/compiler-core/src/type_.rs index eb700ee61c0..04a4d177f42 100644 --- a/compiler-core/src/type_.rs +++ b/compiler-core/src/type_.rs @@ -179,6 +179,7 @@ impl Type { pub fn tuple_types(&self) -> Option>> { match self { Self::Tuple { elems } => Some(elems.clone()), + Self::Var { type_, .. } => type_.borrow().tuple_types(), _ => None, } } @@ -934,6 +935,13 @@ impl TypeVar { } } + pub fn tuple_types(&self) -> Option>> { + match self { + Self::Link { type_ } => type_.tuple_types(), + Self::Unbound { .. } | Self::Generic { .. } => None, + } + } + pub fn constructor_types(&self) -> Option>> { match self { Self::Link { type_ } => type_.constructor_types(), From 52978f6aa3a95ddf030fd3dc55d28cf335560106 Mon Sep 17 00:00:00 2001 From: GearsDatapacks Date: Thu, 26 Dec 2024 10:28:36 +0000 Subject: [PATCH 08/10] Add tests for tuple decoding --- .../src/language_server/code_action.rs | 6 ++-- .../src/language_server/tests/action.rs | 13 +++++++ ...ction__generate_dynamic_decoder_tuple.snap | 35 +++++++++++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder_tuple.snap diff --git a/compiler-core/src/language_server/code_action.rs b/compiler-core/src/language_server/code_action.rs index 5fd46b59205..4467cef793a 100644 --- a/compiler-core/src/language_server/code_action.rs +++ b/compiler-core/src/language_server/code_action.rs @@ -3049,12 +3049,12 @@ impl<'a> RecordLabel<'a> { match self { RecordLabel::Labeled(label) => (*label).into(), RecordLabel::Unlabeled(mut index) => { - let mut name = EcoString::new(); + let mut characters = Vec::new(); let alphabet_length = 26; let alphabet_offset = 'a' as u8; loop { let alphabet_index = (index % alphabet_length) as u8; - name.push((alphabet_offset + alphabet_index) as char); + characters.push((alphabet_offset + alphabet_index) as char); index /= alphabet_length; if index == 0 { @@ -3062,7 +3062,7 @@ impl<'a> RecordLabel<'a> { } index -= 1; } - name + characters.into_iter().rev().collect() } } } diff --git a/compiler-core/src/language_server/tests/action.rs b/compiler-core/src/language_server/tests/action.rs index 5d08acb4b00..99b9d4c39f1 100644 --- a/compiler-core/src/language_server/tests/action.rs +++ b/compiler-core/src/language_server/tests/action.rs @@ -4094,6 +4094,19 @@ pub type Wibble { ); } +#[test] +fn generate_dynamic_decoder_tuple() { + assert_code_action!( + GENERATE_DYNAMIC_DECODER, + " +pub type Wibble { + Wibble(tuple: #(Int, Float, #(String, Bool))) +} +", + find_position_of("type W").to_selection() + ); +} + #[test] fn no_code_action_to_generate_dynamic_decoder_for_multi_variant_type() { assert_no_code_actions!( diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder_tuple.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder_tuple.snap new file mode 100644 index 00000000000..2f7193ad984 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder_tuple.snap @@ -0,0 +1,35 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "\npub type Wibble {\n Wibble(tuple: #(Int, Float, #(String, Bool)))\n}\n" +--- +----- BEFORE ACTION + +pub type Wibble { + ↑ + Wibble(tuple: #(Int, Float, #(String, Bool))) +} + + +----- AFTER ACTION +import gleam/dynamic/decode + +pub type Wibble { + Wibble(tuple: #(Int, Float, #(String, Bool))) +} + +fn wibble_decoder() -> decode.Decoder(Wibble) { + use tuple <- decode.field("tuple", { + use a <- decode.field(0, decode.int) + use b <- decode.field(1, decode.float) + use c <- decode.field(2, { + use a <- decode.field(0, decode.string) + use b <- decode.field(1, decode.bool) + + decode.success(#(a, b)) + }) + + decode.success(#(a, b, c)) + }) + + decode.success(Wibble(tuple:)) +} From 2debc1800e42d33928314d7a3f05f369d22d0747 Mon Sep 17 00:00:00 2001 From: GearsDatapacks Date: Thu, 26 Dec 2024 10:39:07 +0000 Subject: [PATCH 09/10] Add support for recursive decoders --- .../src/language_server/code_action.rs | 17 +++++++++-- .../src/language_server/tests/action.rs | 16 ++++++++++ ...nerate_dynamic_decoder_recursive_type.snap | 29 +++++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder_recursive_type.snap diff --git a/compiler-core/src/language_server/code_action.rs b/compiler-core/src/language_server/code_action.rs index 4467cef793a..a2b3a8bc303 100644 --- a/compiler-core/src/language_server/code_action.rs +++ b/compiler-core/src/language_server/code_action.rs @@ -2942,7 +2942,11 @@ impl<'ast> ast::visit::Visit<'ast> for GenerateDynamicDecoder<'ast> { return; }; - let mut decoder_printer = DecoderPrinter::new(&self.module.ast.names); + let mut decoder_printer = DecoderPrinter::new( + &self.module.ast.names, + custom_type.name.clone(), + self.module.name.clone(), + ); let decoders = fields .iter() @@ -3023,6 +3027,10 @@ fn maybe_import(edits: &mut TextEdits<'_>, module: &Module, module_name: &str) { struct DecoderPrinter<'a> { printer: Printer<'a>, + /// The name of the root type we are printing a decoder for + type_name: EcoString, + /// The module name of the root type we are printing a decoder for + type_module: EcoString, } struct RecordField<'a> { @@ -3069,8 +3077,10 @@ impl<'a> RecordLabel<'a> { } impl<'a> DecoderPrinter<'a> { - fn new(names: &'a Names) -> Self { + fn new(names: &'a Names, type_name: EcoString, type_module: EcoString) -> Self { Self { + type_name, + type_module, printer: Printer::new(names), } } @@ -3132,6 +3142,9 @@ impl<'a> DecoderPrinter<'a> { self.decoder_for(value, indent) ) } + Some((module, name, _)) if module == self.type_module && name == self.type_name => { + eco_format!("{}_decoder()", name.to_snake_case()) + } _ => eco_format!( r#"todo as "Decoder for {}""#, self.printer.print_type(type_) diff --git a/compiler-core/src/language_server/tests/action.rs b/compiler-core/src/language_server/tests/action.rs index 99b9d4c39f1..aa60ca2035b 100644 --- a/compiler-core/src/language_server/tests/action.rs +++ b/compiler-core/src/language_server/tests/action.rs @@ -4107,6 +4107,22 @@ pub type Wibble { ); } +#[test] +fn generate_dynamic_decoder_recursive_type() { + let src = " +import gleam/option + +pub type LinkedList { + LinkedList(value: Int, next: option.Option(LinkedList)) +} +"; + assert_code_action!( + GENERATE_DYNAMIC_DECODER, + TestProject::for_source(src).add_module("gleam/option", "pub type Option(a)"), + find_position_of("type").to_selection() + ); +} + #[test] fn no_code_action_to_generate_dynamic_decoder_for_multi_variant_type() { assert_no_code_actions!( diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder_recursive_type.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder_recursive_type.snap new file mode 100644 index 00000000000..ce60d5b6f06 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_dynamic_decoder_recursive_type.snap @@ -0,0 +1,29 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "\nimport gleam/option\n\npub type LinkedList {\n LinkedList(value: Int, next: option.Option(LinkedList))\n}\n" +--- +----- BEFORE ACTION + +import gleam/option + +pub type LinkedList { + ↑ + LinkedList(value: Int, next: option.Option(LinkedList)) +} + + +----- AFTER ACTION + +import gleam/dynamic/decode +import gleam/option + +pub type LinkedList { + LinkedList(value: Int, next: option.Option(LinkedList)) +} + +fn linked_list_decoder() -> decode.Decoder(LinkedList) { + use value <- decode.field("value", decode.int) + use next <- decode.field("next", decode.optional(linked_list_decoder())) + + decode.success(LinkedList(value:, next:)) +} From 9c7cfac23586eb2edf84af52160c27697a8473f6 Mon Sep 17 00:00:00 2001 From: GearsDatapacks Date: Thu, 26 Dec 2024 10:41:40 +0000 Subject: [PATCH 10/10] Clippy --- compiler-core/src/language_server/code_action.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler-core/src/language_server/code_action.rs b/compiler-core/src/language_server/code_action.rs index a2b3a8bc303..c3a5593ab53 100644 --- a/compiler-core/src/language_server/code_action.rs +++ b/compiler-core/src/language_server/code_action.rs @@ -3043,7 +3043,7 @@ enum RecordLabel<'a> { Unlabeled(usize), } -impl<'a> RecordLabel<'a> { +impl RecordLabel<'_> { fn field_key(&self) -> EcoString { match self { RecordLabel::Labeled(label) => eco_format!("\"{label}\""), @@ -3059,7 +3059,7 @@ impl<'a> RecordLabel<'a> { RecordLabel::Unlabeled(mut index) => { let mut characters = Vec::new(); let alphabet_length = 26; - let alphabet_offset = 'a' as u8; + let alphabet_offset = b'a'; loop { let alphabet_index = (index % alphabet_length) as u8; characters.push((alphabet_offset + alphabet_index) as char);