Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
### v0.7.1 - Unreleased
- **[FEATURE]** Friendlier error when a `#[nutype(...)]` attribute is mistyped: suggests the closest match (e.g. `validte` -> `validate`) and lists the available nutype attributes (see [#240](https://github.com/greyblake/nutype/issues/240)).

### v0.7.0 - 2026-04-25
- **[BREAKING]** Rename `derive_unsafe` to `derive_unchecked` (both the feature flag and the attribute).
- **[FEATURE]** Support `cfg_attr` for conditional derives, e.g. `cfg_attr(feature = "serde", derive(Serialize, Deserialize))`. Supports complex predicates and multiple entries.
Expand Down
4 changes: 4 additions & 0 deletions _typos.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[default.extend-words]
# Intentional misspelling used in tests/docs that demonstrate the
# "Did you mean ...?" suggestion for mistyped nutype attributes.
validte = "validte"
54 changes: 52 additions & 2 deletions nutype_macros/src/common/parse/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -381,8 +381,10 @@ where
return Err(syn::Error::new(ident.span(), msg));
}
} else {
let msg = format!("Unknown attribute `{ident}`");
return Err(syn::Error::new(ident.span(), msg));
return Err(syn::Error::new(
ident.span(),
unknown_top_level_attribute_message(&ident.to_string()),
));
}

// Parse `,` unless it's the end of the stream
Expand All @@ -395,6 +397,54 @@ where
}
}

fn known_top_level_attributes() -> Vec<&'static str> {
let mut names: Vec<&'static str> = vec![
"sanitize",
"validate",
"derive",
"default",
"const_fn",
"cfg_attr",
"constructor",
];
if cfg!(feature = "new_unchecked") {
names.push("new_unchecked");
}
if cfg!(feature = "derive_unchecked") {
names.push("derive_unchecked");
}
names
}

fn unknown_top_level_attribute_message(ident: &str) -> String {
use crate::utils::levenshtein::closest_match;

let known = known_top_level_attributes();
let suggestion = closest_match(ident, &known, 2);

let format_list = |names: &[&str]| -> String {
names
.iter()
.map(|n| format!("`{n}`"))
.collect::<Vec<_>>()
.join(", ")
};

match suggestion {
Some(suggested) => {
let others: Vec<&str> = known.iter().copied().filter(|n| *n != suggested).collect();
format!(
"Unknown nutype attribute `{ident}`. Did you mean `{suggested}`?\nOther available nutype attributes are: {}.",
format_list(&others)
)
}
None => format!(
"Unknown nutype attribute `{ident}`.\nAvailable nutype attributes are: {}.",
format_list(&known)
),
}
}

pub fn parse_number<T>(input: ParseStream) -> syn::Result<(T, Span)>
where
T: FromStr,
Expand Down
68 changes: 68 additions & 0 deletions nutype_macros/src/utils/levenshtein.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/// Compute the Levenshtein edit distance between two strings.
///
/// Used to suggest a likely-intended attribute name when the user mistypes one,
/// e.g. `validte` -> `validate`.
pub fn levenshtein(a: &str, b: &str) -> usize {
let a: Vec<char> = a.chars().collect();
let b: Vec<char> = b.chars().collect();
let m = a.len();
let n = b.len();
if m == 0 {
return n;
}
if n == 0 {
return m;
}

let mut prev: Vec<usize> = (0..=n).collect();
let mut curr: Vec<usize> = vec![0; n + 1];
for i in 1..=m {
curr[0] = i;
for j in 1..=n {
let cost = if a[i - 1] == b[j - 1] { 0 } else { 1 };
curr[j] = (curr[j - 1] + 1).min(prev[j] + 1).min(prev[j - 1] + cost);
}
core::mem::swap(&mut prev, &mut curr);
}
prev[n]
}

/// Return the candidate closest to `query` if its edit distance is `<= max_distance`.
/// Ties are broken by the order in `candidates`.
pub fn closest_match<'a>(
query: &str,
candidates: &[&'a str],
max_distance: usize,
) -> Option<&'a str> {
let mut best: Option<(usize, &'a str)> = None;
for cand in candidates {
let d = levenshtein(query, cand);
if d <= max_distance && best.map(|(bd, _)| d < bd).unwrap_or(true) {
best = Some((d, cand));
}
}
best.map(|(_, s)| s)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn distance_basics() {
assert_eq!(levenshtein("", ""), 0);
assert_eq!(levenshtein("abc", ""), 3);
assert_eq!(levenshtein("", "abc"), 3);
assert_eq!(levenshtein("abc", "abc"), 0);
assert_eq!(levenshtein("validte", "validate"), 1);
assert_eq!(levenshtein("kitten", "sitting"), 3);
}

#[test]
fn picks_closest() {
let candidates = ["sanitize", "validate", "derive", "default"];
assert_eq!(closest_match("validte", &candidates, 2), Some("validate"));
assert_eq!(closest_match("derve", &candidates, 2), Some("derive"));
assert_eq!(closest_match("xyz", &candidates, 2), None);
}
}
1 change: 1 addition & 0 deletions nutype_macros/src/utils/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod issue_reporter;
pub mod levenshtein;
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
error: The `error` attribute requires an accompanying `with` attribute.
Please provide the validation function that returns Result<(), NumError>.
--> tests/ui/common/custom_validaiton_no_with.rs:4:30
--> tests/ui/common/custom_validation_no_with.rs:4:30
|
4 | validate(error = NumError)
| ^
6 changes: 6 additions & 0 deletions test_suite/tests/ui/common/unknown_top_level_attribute.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
use nutype::nutype;

#[nutype(validte)]
pub struct MaxPositionPercentage(i32);

fn main() {}
6 changes: 6 additions & 0 deletions test_suite/tests/ui/common/unknown_top_level_attribute.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
error: Unknown nutype attribute `validte`. Did you mean `validate`?
Other available nutype attributes are: `sanitize`, `derive`, `default`, `const_fn`, `cfg_attr`, `constructor`.
--> tests/ui/common/unknown_top_level_attribute.rs:3:10
|
3 | #[nutype(validte)]
| ^^^^^^^
Loading