diff --git a/crates/aiken-lang/src/tipo/environment.rs b/crates/aiken-lang/src/tipo/environment.rs index ce4992cb..a6055a72 100644 --- a/crates/aiken-lang/src/tipo/environment.rs +++ b/crates/aiken-lang/src/tipo/environment.rs @@ -783,21 +783,13 @@ impl<'a> Environment<'a> { }; } else if !value_imported { // Error if no type or value was found with that name - return Err(Error::UnknownModuleField { - location: *location, - name: name.clone(), - module_name: module.join("/"), - value_constructors: module_info - .values - .keys() - .map(|t| t.to_string()) - .collect(), - type_constructors: module_info - .types - .keys() - .map(|t| t.to_string()) - .collect(), - }); + return Err(Error::unknown_module_field( + *location, + name.clone(), + module.join("/"), + module_info.values.keys().map(|t| t.to_string()).collect(), + module_info.types.keys().map(|t| t.to_string()).collect(), + )); } } diff --git a/crates/aiken-lang/src/tipo/error.rs b/crates/aiken-lang/src/tipo/error.rs index 92496ec0..ae6f0fa3 100644 --- a/crates/aiken-lang/src/tipo/error.rs +++ b/crates/aiken-lang/src/tipo/error.rs @@ -4,7 +4,10 @@ use ordinal::Ordinal; use miette::Diagnostic; -use crate::ast::{BinOp, Span, TodoKind}; +use crate::{ + ast::{BinOp, Span, TodoKind}, + levenshtein, +}; use super::Type; @@ -175,13 +178,15 @@ pub enum Error { imported_modules: Vec, }, - #[error("Unknown module field\n\n{name}\n\nin module\n\n{module_name}\n")] + #[error("Unknown import '{name}' from module '{module_name}'\n")] + #[diagnostic()] UnknownModuleField { + #[label] location: Span, name: String, module_name: String, - value_constructors: Vec, - type_constructors: Vec, + #[help] + hint: Option, }, #[error("Unknown module value\n\n{name}\n")] @@ -419,6 +424,37 @@ impl Error { other => other, } } + + pub fn unknown_module_field( + location: Span, + name: String, + module_name: String, + value_constructors: Vec, + type_constructors: Vec, + ) -> Self { + let mut candidates = vec![]; + candidates.extend(value_constructors); + candidates.extend(type_constructors); + + let hint = candidates + .iter() + .map(|s| (s, levenshtein::distance(&name, s))) + .min_by(|(_, a), (_, b)| a.cmp(b)) + .and_then(|(suggestion, distance)| { + if distance <= 5 { + Some(format!("Did you mean to import '{suggestion}'?")) + } else { + None + } + }); + + Self::UnknownModuleField { + location, + name, + module_name, + hint, + } + } } #[derive(Debug, PartialEq, Clone, thiserror::Error, Diagnostic)]