use crate::span::Span;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Severity {
Error,
Warning,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCode {
E001, E002, E003,
E010, E011, E012, E013,
E020, E021, E022,
E030, E031,
E040, E041, E042, E043, E044, E045,
E050, E051, E052, E053, E054, E055, E056, E057, E058, E059, E060, E061, E062, E063, E064, E065, E066,
E070, E071, E072, E073,
E080, E081, E082, E083, E084, E085, E086, E087, E088, E089, E090, E091,
E100, E101, E102, E103, E104, E105, E106, E107, E108, E109, E110, E111, E112, E113, E114, E115,
E120, E121, E122, E123, E124, E125, E126,
E130, E131, E132, E133, E134, W135,
E140, E141, E142, E143, E144, E145, E146, E147,
E999, }
impl ErrorCode {
pub fn as_str(&self) -> &'static str {
match self {
ErrorCode::E001 => "E001",
ErrorCode::E002 => "E002",
ErrorCode::E003 => "E003",
ErrorCode::E010 => "E010",
ErrorCode::E011 => "E011",
ErrorCode::E012 => "E012",
ErrorCode::E013 => "E013",
ErrorCode::E020 => "E020",
ErrorCode::E021 => "E021",
ErrorCode::E022 => "E022",
ErrorCode::E030 => "E030",
ErrorCode::E031 => "E031",
ErrorCode::E040 => "E040",
ErrorCode::E041 => "E041",
ErrorCode::E042 => "E042",
ErrorCode::E043 => "E043",
ErrorCode::E044 => "E044",
ErrorCode::E045 => "E045",
ErrorCode::E050 => "E050",
ErrorCode::E051 => "E051",
ErrorCode::E052 => "E052",
ErrorCode::E053 => "E053",
ErrorCode::E054 => "E054",
ErrorCode::E055 => "E055",
ErrorCode::E056 => "E056",
ErrorCode::E057 => "E057",
ErrorCode::E058 => "E058",
ErrorCode::E059 => "E059",
ErrorCode::E060 => "E060",
ErrorCode::E061 => "E061",
ErrorCode::E062 => "E062",
ErrorCode::E063 => "E063",
ErrorCode::E064 => "E064",
ErrorCode::E065 => "E065",
ErrorCode::E066 => "E066",
ErrorCode::E070 => "E070",
ErrorCode::E071 => "E071",
ErrorCode::E072 => "E072",
ErrorCode::E073 => "E073",
ErrorCode::E080 => "E080",
ErrorCode::E081 => "E081",
ErrorCode::E082 => "E082",
ErrorCode::E083 => "E083",
ErrorCode::E084 => "E084",
ErrorCode::E085 => "E085",
ErrorCode::E086 => "E086",
ErrorCode::E087 => "E087",
ErrorCode::E088 => "E088",
ErrorCode::E089 => "E089",
ErrorCode::E090 => "E090",
ErrorCode::E091 => "E091",
ErrorCode::E100 => "E100",
ErrorCode::E101 => "E101",
ErrorCode::E102 => "E102",
ErrorCode::E103 => "E103",
ErrorCode::E104 => "E104",
ErrorCode::E105 => "E105",
ErrorCode::E106 => "E106",
ErrorCode::E107 => "E107",
ErrorCode::E108 => "E108",
ErrorCode::E109 => "E109",
ErrorCode::E110 => "E110",
ErrorCode::E111 => "E111",
ErrorCode::E112 => "E112",
ErrorCode::E113 => "E113",
ErrorCode::E114 => "E114",
ErrorCode::E115 => "E115",
ErrorCode::E120 => "E120",
ErrorCode::E121 => "E121",
ErrorCode::E122 => "E122",
ErrorCode::E123 => "E123",
ErrorCode::E124 => "E124",
ErrorCode::E125 => "E125",
ErrorCode::E126 => "E126",
ErrorCode::E130 => "E130",
ErrorCode::E131 => "E131",
ErrorCode::E132 => "E132",
ErrorCode::E133 => "E133",
ErrorCode::E134 => "E134",
ErrorCode::W135 => "W135",
ErrorCode::E140 => "E140",
ErrorCode::E141 => "E141",
ErrorCode::E142 => "E142",
ErrorCode::E143 => "E143",
ErrorCode::E144 => "E144",
ErrorCode::E145 => "E145",
ErrorCode::E146 => "E146",
ErrorCode::E147 => "E147",
ErrorCode::E999 => "E999",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ErrorClass {
InvalidCharacter,
InvalidEscape,
UnterminatedString,
MissingNamespace,
DuplicateNamespace,
ImportAfterDecl,
ImportNamedAliasedCombined,
NamespaceInvalidComponent,
NamespaceReserved,
NamespaceEmpty,
DeclNameInvalid,
DeclNameDuplicate,
FieldNameInvalid,
FieldNameDuplicate,
OrdinalDuplicate,
OrdinalTooLarge,
OrdinalReusedAfterRemoved,
UnknownType,
ConfigTypeAsField,
NewtypeOverNewtype,
NewtypeOverConfig,
AliasTargetIsAlias,
AliasCycleDetected,
AliasTargetNotFound,
ConstTypeInvalid,
ConstCycleDetected,
ConstDivByZero,
ConstRefNotFound,
InvalidMapKey,
GeometricInvalidElementType,
ConfigMissingDefault,
ConfigHasOrdinal,
ConfigInvalidType,
ConfigEncodingAnnotation,
EnumOrdinalDuplicate,
EnumOrdinalTooLarge,
EnumBackingTooNarrow,
EnumBackingInvalid,
EnumVariantNameInvalid,
FlagsBitTooHigh,
BitsInlineEmpty,
InvalidBitName,
UnionOrdinalDuplicate,
UnionOrdinalTooLarge,
UnionVariantNameInvalid,
DuplicateAnnotation,
NonExhaustiveInvalidTarget,
DeprecatedMissingReason,
RemovedMissingReason,
LimitInvalidTarget,
LimitExceedsGlobal,
LimitZero,
VarintInvalidTarget,
ZigzagInvalidTarget,
VarintZigzagCombined,
DeltaInvalidTarget,
TypeValueOverflow,
VersionAfterNamespace,
VersionDuplicate,
VersionInvalidSemver,
RecursiveTypeInfinite,
EncodingTypeMismatch,
UnresolvedType,
WhereClauseTypeMismatch,
WhereClauseRangeInvalid,
WhereClauseLenOnNonCollection,
WhereClauseConstRefNotFound,
WhereClauseOperatorInvalid,
InvariantUnsupported,
ImplFnExternal,
ImportVersionMismatch,
ImportVersionUnavailable,
TraitFnDuplicate,
FnParamDuplicate,
ImplFnMissing,
ImplFnExtra,
ImplFnSignatureMismatch,
ImplFnBodyTypeMismatch,
ImplFnReturnMismatch,
ImplFnAssignmentInvalid,
UnexpectedToken,
UnexpectedEof,
}
impl ErrorClass {
pub fn to_code(&self) -> ErrorCode {
match self {
ErrorClass::InvalidCharacter => ErrorCode::E001,
ErrorClass::InvalidEscape => ErrorCode::E002,
ErrorClass::UnterminatedString => ErrorCode::E003,
ErrorClass::MissingNamespace => ErrorCode::E010,
ErrorClass::DuplicateNamespace => ErrorCode::E011,
ErrorClass::ImportAfterDecl => ErrorCode::E012,
ErrorClass::ImportNamedAliasedCombined => ErrorCode::E013,
ErrorClass::NamespaceInvalidComponent => ErrorCode::E020,
ErrorClass::NamespaceReserved => ErrorCode::E021,
ErrorClass::NamespaceEmpty => ErrorCode::E022,
ErrorClass::DeclNameInvalid => ErrorCode::E030,
ErrorClass::DeclNameDuplicate => ErrorCode::E031,
ErrorClass::FieldNameInvalid => ErrorCode::E040,
ErrorClass::FieldNameDuplicate => ErrorCode::E041,
ErrorClass::OrdinalDuplicate => ErrorCode::E042,
ErrorClass::OrdinalTooLarge => ErrorCode::E043,
ErrorClass::OrdinalReusedAfterRemoved => ErrorCode::E044,
ErrorClass::UnknownType => ErrorCode::E050,
ErrorClass::ConfigTypeAsField => ErrorCode::E052,
ErrorClass::NewtypeOverNewtype => ErrorCode::E053,
ErrorClass::NewtypeOverConfig => ErrorCode::E054,
ErrorClass::AliasTargetIsAlias => ErrorCode::E055,
ErrorClass::AliasCycleDetected => ErrorCode::E056,
ErrorClass::AliasTargetNotFound => ErrorCode::E057,
ErrorClass::ConstTypeInvalid => ErrorCode::E058,
ErrorClass::ConstCycleDetected => ErrorCode::E059,
ErrorClass::ConstDivByZero => ErrorCode::E060,
ErrorClass::ConstRefNotFound => ErrorCode::E061,
ErrorClass::InvalidMapKey => ErrorCode::E062,
ErrorClass::GeometricInvalidElementType => ErrorCode::E063,
ErrorClass::RecursiveTypeInfinite => ErrorCode::E064,
ErrorClass::EncodingTypeMismatch => ErrorCode::E065,
ErrorClass::UnresolvedType => ErrorCode::E066,
ErrorClass::ConfigMissingDefault => ErrorCode::E070,
ErrorClass::ConfigHasOrdinal => ErrorCode::E071,
ErrorClass::ConfigInvalidType => ErrorCode::E072,
ErrorClass::ConfigEncodingAnnotation => ErrorCode::E073,
ErrorClass::EnumOrdinalDuplicate => ErrorCode::E080,
ErrorClass::EnumOrdinalTooLarge => ErrorCode::E081,
ErrorClass::EnumBackingTooNarrow => ErrorCode::E082,
ErrorClass::EnumBackingInvalid => ErrorCode::E083,
ErrorClass::EnumVariantNameInvalid => ErrorCode::E084,
ErrorClass::FlagsBitTooHigh => ErrorCode::E085,
ErrorClass::BitsInlineEmpty => ErrorCode::E086,
ErrorClass::InvalidBitName => ErrorCode::E087,
ErrorClass::UnionOrdinalDuplicate => ErrorCode::E088,
ErrorClass::UnionOrdinalTooLarge => ErrorCode::E089,
ErrorClass::UnionVariantNameInvalid => ErrorCode::E090,
ErrorClass::DuplicateAnnotation => ErrorCode::E100,
ErrorClass::NonExhaustiveInvalidTarget => ErrorCode::E115,
ErrorClass::DeprecatedMissingReason => ErrorCode::E102,
ErrorClass::RemovedMissingReason => ErrorCode::E103,
ErrorClass::LimitInvalidTarget => ErrorCode::E104,
ErrorClass::LimitExceedsGlobal => ErrorCode::E105,
ErrorClass::LimitZero => ErrorCode::E106,
ErrorClass::VarintInvalidTarget => ErrorCode::E107,
ErrorClass::ZigzagInvalidTarget => ErrorCode::E108,
ErrorClass::VarintZigzagCombined => ErrorCode::E109,
ErrorClass::DeltaInvalidTarget => ErrorCode::E110,
ErrorClass::TypeValueOverflow => ErrorCode::E111,
ErrorClass::VersionAfterNamespace => ErrorCode::E112,
ErrorClass::VersionDuplicate => ErrorCode::E113,
ErrorClass::VersionInvalidSemver => ErrorCode::E114,
ErrorClass::WhereClauseTypeMismatch => ErrorCode::E120,
ErrorClass::WhereClauseRangeInvalid => ErrorCode::E121,
ErrorClass::WhereClauseLenOnNonCollection => ErrorCode::E122,
ErrorClass::WhereClauseConstRefNotFound => ErrorCode::E123,
ErrorClass::WhereClauseOperatorInvalid => ErrorCode::E124,
ErrorClass::InvariantUnsupported => ErrorCode::E126,
ErrorClass::ImplFnExternal => ErrorCode::E133,
ErrorClass::ImportVersionMismatch => ErrorCode::E134,
ErrorClass::ImportVersionUnavailable => ErrorCode::W135,
ErrorClass::TraitFnDuplicate => ErrorCode::E140,
ErrorClass::FnParamDuplicate => ErrorCode::E141,
ErrorClass::ImplFnMissing => ErrorCode::E142,
ErrorClass::ImplFnExtra => ErrorCode::E143,
ErrorClass::ImplFnSignatureMismatch => ErrorCode::E144,
ErrorClass::ImplFnBodyTypeMismatch => ErrorCode::E145,
ErrorClass::ImplFnReturnMismatch => ErrorCode::E146,
ErrorClass::ImplFnAssignmentInvalid => ErrorCode::E147,
ErrorClass::UnexpectedToken => ErrorCode::E999,
ErrorClass::UnexpectedEof => ErrorCode::E999,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Note {
Note(String),
DidYouMean(String),
ValidOptions(Vec<String>),
ExpectedVsActual { expected: String, actual: String },
Help(String),
}
impl Note {
pub fn format(&self) -> String {
match self {
Note::Note(msg) => format!("note: {msg}"),
Note::DidYouMean(suggestion) => format!("help: did you mean `{suggestion}`?"),
Note::ValidOptions(options) => {
let list = options.join(", ");
format!("note: valid options are: {list}")
}
Note::ExpectedVsActual { expected, actual } => {
format!("note: expected `{expected}`, found `{actual}`")
}
Note::Help(msg) => format!("help: {msg}"),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Diagnostic {
pub severity: Severity,
pub span: Span,
pub class: ErrorClass,
pub message: String,
pub source_file: Option<std::path::PathBuf>,
pub notes: Vec<Note>,
pub code: ErrorCode,
}
impl Diagnostic {
pub fn error(span: Span, class: ErrorClass, message: impl Into<String>) -> Self {
let class_clone = class.clone();
Self {
severity: Severity::Error,
span,
class,
message: message.into(),
source_file: None,
notes: Vec::new(),
code: class_clone.to_code(),
}
}
pub fn warning(span: Span, class: ErrorClass, message: impl Into<String>) -> Self {
let class_clone = class.clone();
Self {
severity: Severity::Warning,
span,
class,
message: message.into(),
source_file: None,
notes: Vec::new(),
code: class_clone.to_code(),
}
}
pub fn with_file(mut self, path: std::path::PathBuf) -> Self {
self.source_file = Some(path);
self
}
pub fn with_note(mut self, note: Note) -> Self {
self.notes.push(note);
self
}
pub fn with_notes(mut self, notes: impl IntoIterator<Item = Note>) -> Self {
self.notes.extend(notes);
self
}
pub fn with_suggestion(self, suggestion: impl Into<String>) -> Self {
self.with_note(Note::DidYouMean(suggestion.into()))
}
pub fn with_expected_vs_actual(
self,
expected: impl Into<String>,
actual: impl Into<String>,
) -> Self {
self.with_note(Note::ExpectedVsActual {
expected: expected.into(),
actual: actual.into(),
})
}
pub fn with_valid_options(self, options: impl IntoIterator<Item = impl Into<String>>) -> Self {
let opts: Vec<String> = options.into_iter().map(|s| s.into()).collect();
self.with_note(Note::ValidOptions(opts))
}
pub fn with_help(self, help: impl Into<String>) -> Self {
self.with_note(Note::Help(help.into()))
}
}
pub fn edit_distance(a: &str, b: &str) -> usize {
let a_len = a.chars().count();
let b_len = b.chars().count();
if a_len == 0 {
return b_len;
}
if b_len == 0 {
return a_len;
}
let mut prev_row: Vec<usize> = (0..=b_len).collect();
let mut curr_row = vec![0; b_len + 1];
for (i, a_char) in a.chars().enumerate() {
curr_row[0] = i + 1;
for (j, b_char) in b.chars().enumerate() {
let cost = if a_char == b_char { 0 } else { 1 };
curr_row[j + 1] = (prev_row[j + 1] + 1) .min(curr_row[j] + 1) .min(prev_row[j] + cost); }
std::mem::swap(&mut prev_row, &mut curr_row);
}
prev_row[b_len]
}
pub fn find_closest_match<'a>(
target: &str,
candidates: impl Iterator<Item = &'a str>,
) -> Option<&'a str> {
let target_lower = target.to_lowercase();
let mut best_match: Option<&str> = None;
let mut best_distance = usize::MAX;
let threshold = (target.len() / 3).max(3);
for candidate in candidates {
let candidate_lower = candidate.to_lowercase();
let distance = edit_distance(&target_lower, &candidate_lower);
if distance == 0 {
return Some(candidate);
}
if distance < best_distance && distance <= threshold {
best_distance = distance;
best_match = Some(candidate);
}
}
best_match
}
pub fn find_similar_matches<'a>(
target: &str,
candidates: impl Iterator<Item = &'a str>,
max_results: usize,
) -> Vec<&'a str> {
let target_lower = target.to_lowercase();
let threshold = (target.len() / 3).max(3);
let mut matches: Vec<(&str, usize)> = candidates
.map(|c| {
let dist = edit_distance(&target_lower, &c.to_lowercase());
(c, dist)
})
.filter(|(_, d)| *d <= threshold && *d > 0) .collect();
matches.sort_by(|(a, da), (b, db)| da.cmp(db).then(a.cmp(b)));
matches.truncate(max_results);
matches.into_iter().map(|(s, _)| s).collect()
}
pub fn format_diagnostic_simple(diag: &Diagnostic, filename: Option<&str>) -> String {
let severity_str = match diag.severity {
Severity::Error => "error",
Severity::Warning => "warning",
};
let mut result = if let Some(file) = filename {
format!(
"{}[{}]: {}\n --> {}:{}",
severity_str,
diag.code.as_str(),
diag.message,
file,
diag.span.offset
)
} else {
format!("{}[{}]: {}", severity_str, diag.code.as_str(), diag.message)
};
for note in &diag.notes {
result.push('\n');
result.push_str(¬e.format());
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_edit_distance() {
assert_eq!(edit_distance("", ""), 0);
assert_eq!(edit_distance("a", ""), 1);
assert_eq!(edit_distance("", "a"), 1);
assert_eq!(edit_distance("abc", "abc"), 0);
assert_eq!(edit_distance("abc", "def"), 3);
assert_eq!(edit_distance("kitten", "sitting"), 3);
assert_eq!(edit_distance("sunday", "saturday"), 3);
}
#[test]
fn test_find_closest_match() {
let candidates = ["username", "user_id", "user_name", "email", "password"];
let iter = candidates.iter().copied();
assert_eq!(find_closest_match("email", iter.clone()), Some("email"));
assert_eq!(
find_closest_match("usrname", iter.clone()),
Some("username")
);
assert_eq!(find_closest_match("xyz", iter.clone()), None);
assert_eq!(find_closest_match("EMAIL", iter.clone()), Some("email"));
}
#[test]
fn test_find_similar_matches() {
let candidates = ["username", "user_id", "user_name", "email", "password"];
let iter = candidates.iter().copied();
let similar = find_similar_matches("usrname", iter.clone(), 3);
assert!(!similar.is_empty());
assert!(similar.contains(&"username"));
}
#[test]
fn test_note_format() {
assert_eq!(
Note::Note("additional context".to_string()).format(),
"note: additional context"
);
assert_eq!(
Note::DidYouMean("foo".to_string()).format(),
"help: did you mean `foo`?"
);
assert_eq!(
Note::ValidOptions(vec!["a".to_string(), "b".to_string()]).format(),
"note: valid options are: a, b"
);
assert_eq!(
Note::ExpectedVsActual {
expected: "u32".to_string(),
actual: "string".to_string()
}
.format(),
"note: expected `u32`, found `string`"
);
}
}