Skip to content

Commit a59f050

Browse files
authored
Merge pull request #243 from greyblake/friendly-error-message-when-attribute-is-mistyped
Friendly error message when attribute is mistyped
2 parents 9f5ab12 + d5c0cb8 commit a59f050

9 files changed

Lines changed: 141 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
### v0.7.1 - Unreleased
2+
- **[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)).
3+
14
### v0.7.0 - 2026-04-25
25
- **[BREAKING]** Rename `derive_unsafe` to `derive_unchecked` (both the feature flag and the attribute).
36
- **[FEATURE]** Support `cfg_attr` for conditional derives, e.g. `cfg_attr(feature = "serde", derive(Serialize, Deserialize))`. Supports complex predicates and multiple entries.

_typos.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[default.extend-words]
2+
# Intentional misspelling used in tests/docs that demonstrate the
3+
# "Did you mean ...?" suggestion for mistyped nutype attributes.
4+
validte = "validte"

nutype_macros/src/common/parse/mod.rs

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -381,8 +381,10 @@ where
381381
return Err(syn::Error::new(ident.span(), msg));
382382
}
383383
} else {
384-
let msg = format!("Unknown attribute `{ident}`");
385-
return Err(syn::Error::new(ident.span(), msg));
384+
return Err(syn::Error::new(
385+
ident.span(),
386+
unknown_top_level_attribute_message(&ident.to_string()),
387+
));
386388
}
387389

388390
// Parse `,` unless it's the end of the stream
@@ -395,6 +397,54 @@ where
395397
}
396398
}
397399

400+
fn known_top_level_attributes() -> Vec<&'static str> {
401+
let mut names: Vec<&'static str> = vec![
402+
"sanitize",
403+
"validate",
404+
"derive",
405+
"default",
406+
"const_fn",
407+
"cfg_attr",
408+
"constructor",
409+
];
410+
if cfg!(feature = "new_unchecked") {
411+
names.push("new_unchecked");
412+
}
413+
if cfg!(feature = "derive_unchecked") {
414+
names.push("derive_unchecked");
415+
}
416+
names
417+
}
418+
419+
fn unknown_top_level_attribute_message(ident: &str) -> String {
420+
use crate::utils::levenshtein::closest_match;
421+
422+
let known = known_top_level_attributes();
423+
let suggestion = closest_match(ident, &known, 2);
424+
425+
let format_list = |names: &[&str]| -> String {
426+
names
427+
.iter()
428+
.map(|n| format!("`{n}`"))
429+
.collect::<Vec<_>>()
430+
.join(", ")
431+
};
432+
433+
match suggestion {
434+
Some(suggested) => {
435+
let others: Vec<&str> = known.iter().copied().filter(|n| *n != suggested).collect();
436+
format!(
437+
"Unknown nutype attribute `{ident}`. Did you mean `{suggested}`?\nOther available nutype attributes are: {}.",
438+
format_list(&others)
439+
)
440+
}
441+
None => format!(
442+
"Unknown nutype attribute `{ident}`.\nAvailable nutype attributes are: {}.",
443+
format_list(&known)
444+
),
445+
}
446+
}
447+
398448
pub fn parse_number<T>(input: ParseStream) -> syn::Result<(T, Span)>
399449
where
400450
T: FromStr,
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/// Compute the Levenshtein edit distance between two strings.
2+
///
3+
/// Used to suggest a likely-intended attribute name when the user mistypes one,
4+
/// e.g. `validte` -> `validate`.
5+
pub fn levenshtein(a: &str, b: &str) -> usize {
6+
let a: Vec<char> = a.chars().collect();
7+
let b: Vec<char> = b.chars().collect();
8+
let m = a.len();
9+
let n = b.len();
10+
if m == 0 {
11+
return n;
12+
}
13+
if n == 0 {
14+
return m;
15+
}
16+
17+
let mut prev: Vec<usize> = (0..=n).collect();
18+
let mut curr: Vec<usize> = vec![0; n + 1];
19+
for i in 1..=m {
20+
curr[0] = i;
21+
for j in 1..=n {
22+
let cost = if a[i - 1] == b[j - 1] { 0 } else { 1 };
23+
curr[j] = (curr[j - 1] + 1).min(prev[j] + 1).min(prev[j - 1] + cost);
24+
}
25+
core::mem::swap(&mut prev, &mut curr);
26+
}
27+
prev[n]
28+
}
29+
30+
/// Return the candidate closest to `query` if its edit distance is `<= max_distance`.
31+
/// Ties are broken by the order in `candidates`.
32+
pub fn closest_match<'a>(
33+
query: &str,
34+
candidates: &[&'a str],
35+
max_distance: usize,
36+
) -> Option<&'a str> {
37+
let mut best: Option<(usize, &'a str)> = None;
38+
for cand in candidates {
39+
let d = levenshtein(query, cand);
40+
if d <= max_distance && best.map(|(bd, _)| d < bd).unwrap_or(true) {
41+
best = Some((d, cand));
42+
}
43+
}
44+
best.map(|(_, s)| s)
45+
}
46+
47+
#[cfg(test)]
48+
mod tests {
49+
use super::*;
50+
51+
#[test]
52+
fn distance_basics() {
53+
assert_eq!(levenshtein("", ""), 0);
54+
assert_eq!(levenshtein("abc", ""), 3);
55+
assert_eq!(levenshtein("", "abc"), 3);
56+
assert_eq!(levenshtein("abc", "abc"), 0);
57+
assert_eq!(levenshtein("validte", "validate"), 1);
58+
assert_eq!(levenshtein("kitten", "sitting"), 3);
59+
}
60+
61+
#[test]
62+
fn picks_closest() {
63+
let candidates = ["sanitize", "validate", "derive", "default"];
64+
assert_eq!(closest_match("validte", &candidates, 2), Some("validate"));
65+
assert_eq!(closest_match("derve", &candidates, 2), Some("derive"));
66+
assert_eq!(closest_match("xyz", &candidates, 2), None);
67+
}
68+
}

nutype_macros/src/utils/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
pub mod issue_reporter;
2+
pub mod levenshtein;

test_suite/tests/ui/common/custom_validaiton_no_with.rs renamed to test_suite/tests/ui/common/custom_validation_no_with.rs

File renamed without changes.

test_suite/tests/ui/common/custom_validaiton_no_with.stderr renamed to test_suite/tests/ui/common/custom_validation_no_with.stderr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
error: The `error` attribute requires an accompanying `with` attribute.
22
Please provide the validation function that returns Result<(), NumError>.
3-
--> tests/ui/common/custom_validaiton_no_with.rs:4:30
3+
--> tests/ui/common/custom_validation_no_with.rs:4:30
44
|
55
4 | validate(error = NumError)
66
| ^
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
use nutype::nutype;
2+
3+
#[nutype(validte)]
4+
pub struct MaxPositionPercentage(i32);
5+
6+
fn main() {}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
error: Unknown nutype attribute `validte`. Did you mean `validate`?
2+
Other available nutype attributes are: `sanitize`, `derive`, `default`, `const_fn`, `cfg_attr`, `constructor`.
3+
--> tests/ui/common/unknown_top_level_attribute.rs:3:10
4+
|
5+
3 | #[nutype(validte)]
6+
| ^^^^^^^

0 commit comments

Comments
 (0)