Trigger warning when destructuring records using unnamed fields

Comes with a helpful hint and a LSP quickfix, so that it's most
  convenient.
This commit is contained in:
KtorZ 2025-01-22 18:57:22 +01:00 committed by Lucas
parent b25afa2d0d
commit 05264bc423
3 changed files with 103 additions and 0 deletions

View File

@ -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::<Vec<_>>()
.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::<Vec<_>>()
.join("\n")
}

View File

@ -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::<Vec<_>>();
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(),

View File

@ -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<lsp_types::Diagnostic>),
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<Quickfix> {
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<AnnotatedEdit> {
},
)]
}
fn unused_record_fields(diagnostic: &lsp_types::Diagnostic) -> Vec<AnnotatedEdit> {
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
}