use strsim to suggest similar identifiers in UnknownIdentifier error

This commit is contained in:
Moritz Hölting 2025-08-27 17:05:21 +02:00
parent d9f2d99c3a
commit ef7bf95447
9 changed files with 140 additions and 52 deletions

View File

@ -456,9 +456,10 @@ impl Assignment {
return Err(err); return Err(err);
} }
} else { } else {
let err = error::Error::UnknownIdentifier(UnknownIdentifier { let err = error::Error::UnknownIdentifier(UnknownIdentifier::from_semantic_scope(
identifier: self.destination().span(), self.destination().span(),
}); scope,
));
handler.receive(err.clone()); handler.receive(err.clone());
return Err(err); return Err(err);
} }
@ -574,9 +575,9 @@ impl Primary {
match self { match self {
Self::Identifier(ident) => { Self::Identifier(ident) => {
if scope.get_variable(ident.span.str()).is_none() { if scope.get_variable(ident.span.str()).is_none() {
let err = error::Error::UnknownIdentifier(UnknownIdentifier { let err = error::Error::UnknownIdentifier(
identifier: ident.span.clone(), UnknownIdentifier::from_semantic_scope(ident.span.clone(), scope),
}); );
handler.receive(err.clone()); handler.receive(err.clone());
Err(err) Err(err)
} else { } else {
@ -589,9 +590,12 @@ impl Primary {
let var = scope.get_variable(call.identifier().span.str()); let var = scope.get_variable(call.identifier().span.str());
var.map_or_else( var.map_or_else(
|| { || {
let err = error::Error::UnknownIdentifier(UnknownIdentifier { let err = error::Error::UnknownIdentifier(
identifier: call.identifier().span.clone(), UnknownIdentifier::from_semantic_scope(
}); call.identifier().span.clone(),
scope,
),
);
handler.receive(err.clone()); handler.receive(err.clone());
Err(err) Err(err)
}, },
@ -661,8 +665,8 @@ impl Primary {
Err(err) Err(err)
} }
} }
Self::MemberAccess(_) => { Self::MemberAccess(member_access) => {
// TODO: member_access.parent().analyze_semantics(scope, handler)?;
Ok(()) Ok(())
} }
Self::Parenthesized(expr) => expr.analyze_semantics(scope, handler), Self::Parenthesized(expr) => expr.analyze_semantics(scope, handler),
@ -971,9 +975,12 @@ impl TemplateStringLiteral {
match expression.as_ref() { match expression.as_ref() {
Expression::Primary(Primary::Identifier(identifier)) => { Expression::Primary(Primary::Identifier(identifier)) => {
if scope.get_variable(identifier.span.str()).is_none() { if scope.get_variable(identifier.span.str()).is_none() {
let err = error::Error::UnknownIdentifier(UnknownIdentifier { let err = error::Error::UnknownIdentifier(
identifier: identifier.span.clone(), UnknownIdentifier::from_semantic_scope(
}); identifier.span.clone(),
scope,
),
);
handler.receive(err.clone()); handler.receive(err.clone());
errs.push(err); errs.push(err);
} }
@ -999,9 +1006,12 @@ impl TemplateStringLiteral {
} }
} }
} else { } else {
let err = error::Error::UnknownIdentifier(UnknownIdentifier { let err = error::Error::UnknownIdentifier(
identifier: identifier.span.clone(), UnknownIdentifier::from_semantic_scope(
}); identifier.span.clone(),
scope,
),
);
handler.receive(err.clone()); handler.receive(err.clone());
errs.push(err); errs.push(err);
} }

View File

@ -1,5 +1,7 @@
use std::{collections::HashMap, sync::RwLock}; use std::{collections::HashMap, sync::RwLock};
use crate::{base::source_file::Span, transpile::error::UnknownIdentifier};
/// Type of variable /// Type of variable
#[derive(Clone, Debug, Copy, PartialEq, Eq)] #[derive(Clone, Debug, Copy, PartialEq, Eq)]
pub enum VariableType { pub enum VariableType {
@ -95,3 +97,29 @@ impl<'a> SemanticScope<'a> {
self.parent self.parent
} }
} }
impl UnknownIdentifier {
pub(super) fn from_semantic_scope(identifier: Span, scope: &SemanticScope<'_>) -> Self {
use itertools::Itertools as _;
let own_name = identifier.str();
let alternatives = scope
.get_all_variables()
.iter()
.filter_map(|(name, _)| {
let normalized_distance = strsim::normalized_damerau_levenshtein(own_name, name);
(normalized_distance > 0.8 || strsim::damerau_levenshtein(own_name, name) < 3)
.then_some((normalized_distance, name))
})
.sorted_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal))
.map(|(_, data)| data)
.take(8)
.cloned()
.collect::<Vec<_>>();
Self {
identifier,
alternatives,
}
}
}

View File

@ -303,11 +303,46 @@ impl std::error::Error for AssignmentError {}
pub struct UnknownIdentifier { pub struct UnknownIdentifier {
/// The unknown identifier. /// The unknown identifier.
#[get = "pub"] #[get = "pub"]
pub identifier: Span, pub(crate) identifier: Span,
/// Alternatives to the current identifier
#[get = "pub"]
pub(crate) alternatives: Vec<String>,
}
impl UnknownIdentifier {
#[cfg(feature = "shulkerbox")]
pub(crate) fn from_scope(
identifier: Span,
scope: &std::sync::Arc<super::variables::Scope>,
) -> Self {
use itertools::Itertools as _;
let own_name = identifier.str();
let alternatives = scope
.get_all_variables()
.iter()
.filter_map(|(name, _)| {
let normalized_distance = strsim::normalized_damerau_levenshtein(own_name, name);
(normalized_distance > 0.8 || strsim::damerau_levenshtein(own_name, name) < 3)
.then_some((normalized_distance, name))
})
.sorted_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal))
.map(|(_, data)| data)
.take(8)
.cloned()
.collect::<Vec<_>>();
Self {
identifier,
alternatives,
}
}
} }
impl Display for UnknownIdentifier { impl Display for UnknownIdentifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use std::fmt::Write as _;
write!( write!(
f, f,
"{}", "{}",
@ -317,10 +352,23 @@ impl Display for UnknownIdentifier {
) )
)?; )?;
let help_message = if self.alternatives.is_empty() {
None
} else {
let mut message = String::from("did you mean ");
for (i, alternative) in self.alternatives.iter().enumerate() {
if i > 0 {
message.push_str(", ");
}
let _ = write!(message, "`{alternative}`");
}
Some(message + "?")
};
write!( write!(
f, f,
"\n{}", "\n{}",
SourceCodeDisplay::new(&self.identifier, Option::<u8>::None) SourceCodeDisplay::new(&self.identifier, help_message.as_ref())
) )
} }
} }

View File

@ -1460,9 +1460,10 @@ impl Transpiler {
self.move_data(&from, target, primary, handler) self.move_data(&from, target, primary, handler)
} else { } else {
let err = TranspileError::UnknownIdentifier(UnknownIdentifier { let err = TranspileError::UnknownIdentifier(UnknownIdentifier::from_scope(
identifier: ident.span.clone(), ident.span.clone(),
}); scope,
));
handler.receive(Box::new(err.clone())); handler.receive(Box::new(err.clone()));
Err(err) Err(err)
} }
@ -1587,9 +1588,10 @@ impl Transpiler {
self.move_data(&from, target, primary, handler) self.move_data(&from, target, primary, handler)
} else { } else {
let err = TranspileError::UnknownIdentifier(UnknownIdentifier { let err = TranspileError::UnknownIdentifier(UnknownIdentifier::from_scope(
identifier: ident.span.clone(), ident.span.clone(),
}); scope,
));
handler.receive(Box::new(err.clone())); handler.receive(Box::new(err.clone()));
Err(err) Err(err)
} }
@ -1740,9 +1742,10 @@ impl Transpiler {
} }
} }
} else { } else {
let err = TranspileError::UnknownIdentifier(UnknownIdentifier { let err = TranspileError::UnknownIdentifier(UnknownIdentifier::from_scope(
identifier: ident.span.clone(), ident.span.clone(),
}); scope,
));
handler.receive(Box::new(err.clone())); handler.receive(Box::new(err.clone()));
Err(err) Err(err)
} }
@ -1816,9 +1819,10 @@ impl Transpiler {
} }
} }
} else { } else {
let err = TranspileError::UnknownIdentifier(UnknownIdentifier { let err = TranspileError::UnknownIdentifier(UnknownIdentifier::from_scope(
identifier: ident.span.clone(), ident.span.clone(),
}); scope,
));
handler.receive(Box::new(err.clone())); handler.receive(Box::new(err.clone()));
Err(err) Err(err)
} }

View File

@ -455,9 +455,7 @@ impl Transpiler {
let var = let var =
scope.get_variable(ident.span.str()).ok_or_else(|| { scope.get_variable(ident.span.str()).ok_or_else(|| {
let err = let err =
TranspileError::UnknownIdentifier(UnknownIdentifier { TranspileError::UnknownIdentifier(UnknownIdentifier::from_scope(ident.span(), scope));
identifier: ident.span(),
});
handler.receive(Box::new(err.clone())); handler.receive(Box::new(err.clone()));
err err
})?; })?;

View File

@ -156,9 +156,9 @@ fn print_function(
))), ))),
} }
} else { } else {
Err(TranspileError::UnknownIdentifier(UnknownIdentifier { Err(TranspileError::UnknownIdentifier(
identifier: ident.span(), UnknownIdentifier::from_scope(ident.span(), scope),
})) ))
} }
} }
@ -289,9 +289,9 @@ fn print_function(
))), ))),
} }
} else { } else {
Err(TranspileError::UnknownIdentifier(UnknownIdentifier { Err(TranspileError::UnknownIdentifier(
identifier: ident.span(), UnknownIdentifier::from_scope(ident.span(), scope),
})) ))
} }
} }

View File

@ -295,9 +295,9 @@ mod enabled {
})); }));
} }
None => { None => {
return Err(TranspileError::UnknownIdentifier(UnknownIdentifier { return Err(TranspileError::UnknownIdentifier(
identifier: identifier.span(), UnknownIdentifier::from_scope(identifier.span(), scope),
})); ));
} }
}; };

View File

@ -552,9 +552,10 @@ impl Transpiler {
} }
} }
} else { } else {
let err = TranspileError::UnknownIdentifier(UnknownIdentifier { let err = TranspileError::UnknownIdentifier(UnknownIdentifier::from_scope(
identifier: ident.span.clone(), ident.span.clone(),
}); scope,
));
handler.receive(Box::new(err.clone())); handler.receive(Box::new(err.clone()));
return Err(err); return Err(err);
} }
@ -623,9 +624,10 @@ impl Transpiler {
Err(err) Err(err)
} }
None => { None => {
let err = TranspileError::UnknownIdentifier(UnknownIdentifier { let err = TranspileError::UnknownIdentifier(UnknownIdentifier::from_scope(
identifier: ident.span.clone(), ident.span.clone(),
}); scope,
));
handler.receive(Box::new(err.clone())); handler.receive(Box::new(err.clone()));
Err(err) Err(err)
} }

View File

@ -326,9 +326,7 @@ impl TemplateStringLiteral {
} }
} else { } else {
let err = let err =
TranspileError::UnknownIdentifier(UnknownIdentifier { TranspileError::UnknownIdentifier(UnknownIdentifier::from_scope(identifier.span(), scope));
identifier: identifier.span(),
});
handler.receive(Box::new(err.clone())); handler.receive(Box::new(err.clone()));
Err(err) Err(err)
} }