diff --git a/crates/aiken-lang/src/tipo/error.rs b/crates/aiken-lang/src/tipo/error.rs index 44f36546..5e77fb45 100644 --- a/crates/aiken-lang/src/tipo/error.rs +++ b/crates/aiken-lang/src/tipo/error.rs @@ -1823,6 +1823,16 @@ pub enum Warning { location: Span, value: String, }, + + #[error("I tripped over a confusing constructor destructuring")] + #[diagnostic(help("Try instead: \n\n{}", format_pattern_suggestion(suggestion)))] + #[diagnostic(code("syntax::unused_record_fields"))] + #[diagnostic(url("https://aiken-lang.org/language-tour/custom-types#destructuring"))] + UnusedRecordFields { + #[label("prefer destructuring with named fields")] + location: Span, + suggestion: UntypedPattern, + }, } impl ExtraData for Warning { @@ -1850,6 +1860,10 @@ impl ExtraData for Warning { Warning::UnusedImportedValueOrType { location, .. } => { Some(format!("{},{}", true, location.start)) } + + Warning::UnusedRecordFields { suggestion, .. } => { + Some(Formatter::new().pattern(suggestion).to_pretty_string(80)) + } } } } @@ -1893,3 +1907,20 @@ pub fn format_suggestion(sample: &UntypedExpr) -> String { .collect::>() .join("\n") } + +pub fn format_pattern_suggestion(sample: &UntypedPattern) -> String { + Formatter::new() + .pattern(sample) + .to_pretty_string(70) + .lines() + .enumerate() + .map(|(ix, line)| { + if ix == 0 { + format!("╰─▶ {line}") + } else { + format!(" {line}") + } + }) + .collect::>() + .join("\n") +} diff --git a/crates/aiken-lang/src/tipo/pattern.rs b/crates/aiken-lang/src/tipo/pattern.rs index 86e77903..a93bc753 100644 --- a/crates/aiken-lang/src/tipo/pattern.rs +++ b/crates/aiken-lang/src/tipo/pattern.rs @@ -462,6 +462,50 @@ impl<'a, 'b> PatternTyper<'a, 'b> { } }; + if let Some(field_map) = cons.field_map() { + if !is_record { + let arguments = field_map + .fields + .iter() + .sorted_by(|(a, _), (b, _)| a.cmp(b)) + .zip(pattern_args.iter()) + .filter_map(|((field, (_, _)), arg)| { + if arg.value.is_discard() { + None + } else { + Some(CallArg { + label: Some(field.clone()), + ..arg.clone() + }) + } + }) + .collect::>(); + + let spread_location = if arguments.len() == field_map.fields.len() { + None + } else { + Some(Span { + start: location.end - 3, + end: location.end - 1, + }) + }; + + self.environment.warnings.push(Warning::UnusedRecordFields { + location, + suggestion: Pattern::Constructor { + is_record: true, + location, + name: name.clone(), + arguments, + module: module.clone(), + constructor: (), + spread_location, + tipo: (), + }, + }); + } + } + let instantiated_constructor_type = self.environment.instantiate( constructor_typ, &mut HashMap::new(), diff --git a/crates/aiken-lsp/src/quickfix.rs b/crates/aiken-lsp/src/quickfix.rs index 1f98ba07..ab457608 100644 --- a/crates/aiken-lsp/src/quickfix.rs +++ b/crates/aiken-lsp/src/quickfix.rs @@ -11,6 +11,7 @@ const UNKNOWN_MODULE: &str = "aiken::check::unknown::module"; const UNUSED_IMPORT_VALUE: &str = "aiken::check::unused:import::value"; const UNUSED_IMPORT_MODULE: &str = "aiken::check::unused::import::module"; const USE_LET: &str = "aiken::check::single_constructor_expect"; +const UNUSED_RECORD_FIELDS: &str = "aiken::check::syntax::unused_record_fields"; const UTF8_BYTE_ARRAY_IS_VALID_HEX_STRING: &str = "aiken::check::syntax::bytearray_literal_is_hex_string"; @@ -23,6 +24,7 @@ pub enum Quickfix { UnusedImports(Vec), Utf8ByteArrayIsValidHexString(lsp_types::Diagnostic), UseLet(lsp_types::Diagnostic), + UnusedRecordFields(lsp_types::Diagnostic), } fn match_code( @@ -71,6 +73,10 @@ pub fn assert(diagnostic: lsp_types::Diagnostic) -> Option { return Some(Quickfix::UseLet(diagnostic)); } + if match_code(&diagnostic, Severity::WARNING, UNUSED_RECORD_FIELDS) { + return Some(Quickfix::UnusedRecordFields(diagnostic)); + } + None } @@ -128,6 +134,12 @@ pub fn quickfix( diagnostic, use_let(diagnostic), ), + Quickfix::UnusedRecordFields(diagnostic) => each_as_distinct_action( + &mut actions, + text_document, + diagnostic, + unused_record_fields(diagnostic), + ), }; } @@ -307,3 +319,19 @@ fn use_let(diagnostic: &lsp_types::Diagnostic) -> Vec { }, )] } + +fn unused_record_fields(diagnostic: &lsp_types::Diagnostic) -> Vec { + let mut edits = Vec::new(); + + if let Some(serde_json::Value::String(new_text)) = diagnostic.data.as_ref() { + edits.push(( + "Destructure using named fields".to_string(), + lsp_types::TextEdit { + range: diagnostic.range, + new_text: new_text.clone(), + }, + )); + } + + edits +}