1use std::fmt::{self, Display, Formatter, Write};
4use std::{convert::TryInto, error::Error};
5
6use itertools::{Itertools, Position::*};
7use nom::error::{ErrorKind, FromExternalError, ParseError};
8use pretty_lint::{Position, PrettyLint, Span};
9
10pub(crate) trait WithTagError<I> {
13 fn from_tag(input: I, tag: &'static str) -> Self;
15}
16
17impl<I> WithTagError<I> for () {
18 fn from_tag(_: I, _: &'static str) -> Self {}
19}
20
21#[derive(Debug, Clone, Copy)]
23pub enum Expected {
24 Eof,
26 Char(char),
28 Tag(&'static str),
32 Number,
34}
35
36impl Display for Expected {
37 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
38 match self {
39 Expected::Eof => write!(f, "EoF"),
40 Expected::Number => write!(f, "a number"),
41 Expected::Char(c) => write!(f, "{c:?}"),
42 Expected::Tag(tag) => write!(f, "{tag:?}"),
43 }
44 }
45}
46
47#[derive(Debug, Clone, Copy)]
50pub struct ExpectedAt {
51 pub index: usize,
53
54 pub line: u32,
56
57 pub column: u32,
59
60 pub expected: Expected,
62}
63
64#[derive(Debug, Clone)]
68pub(crate) struct ExpectedContext<'a> {
69 input_tail: &'a str,
70 expected: Expected,
71}
72
73impl<'a> ExpectedContext<'a> {
74 pub fn extract_context(self, input: &'a str) -> ExpectedAt {
77 let offset = input
78 .len()
79 .checked_sub(self.input_tail.len())
80 .expect("input size was smaller than the tail size");
81
82 let prefix = &input[..offset];
83
84 let line_number = prefix.chars().filter(|&c| c == '\n').count() + 1;
85 let last_line_start = prefix
86 .char_indices()
87 .rev()
88 .find(|&(_, c)| c == '\n')
89 .map(|(index, _)| index + 1)
90 .unwrap_or(0);
91 let column_number = (offset - last_line_start) + 1;
92
93 ExpectedAt {
94 line: line_number
95 .try_into()
96 .expect("More than 4 billion lines of input"),
97 column: column_number
98 .try_into()
99 .expect("More than 4 billion columns of input"),
100 index: offset,
101 expected: self.expected,
102 }
103 }
104}
105
106#[derive(Debug, Clone)]
110pub(crate) struct PasswordRulesErrorContext<'a> {
111 expectations: Vec<ExpectedContext<'a>>,
112}
113
114impl<'a> PasswordRulesErrorContext<'a> {
115 pub fn extract_context(self, input: &'a str) -> PasswordRulesError {
118 let mut expectations: Vec<ExpectedAt> = self
119 .expectations
120 .into_iter()
121 .map(|exp| exp.extract_context(input))
122 .collect();
123
124 expectations.sort_unstable_by_key(|exp| exp.index);
125
126 PasswordRulesError { expectations }
127 }
128}
129
130impl<'a> ParseError<&'a str> for PasswordRulesErrorContext<'a> {
131 fn from_error_kind(input: &'a str, kind: ErrorKind) -> Self {
132 match kind {
133 ErrorKind::Eof => Self {
134 expectations: vec![ExpectedContext {
135 input_tail: input,
136 expected: Expected::Eof,
137 }],
138 },
139 ErrorKind::Digit => Self {
140 expectations: vec![ExpectedContext {
141 input_tail: input,
142 expected: Expected::Number,
143 }],
144 },
145 _ => Self {
146 expectations: vec![],
147 },
148 }
149 }
150
151 fn append(input: &'a str, kind: nom::error::ErrorKind, other: Self) -> Self {
152 Self::from_error_kind(input, kind).or(other)
153 }
154
155 fn or(mut self, other: Self) -> Self {
156 self.expectations.extend(other.expectations);
157 self
158 }
159
160 fn from_char(input: &'a str, c: char) -> Self {
161 Self {
162 expectations: vec![ExpectedContext {
163 input_tail: input,
164 expected: Expected::Char(c),
165 }],
166 }
167 }
168}
169
170impl<'a> WithTagError<&'a str> for PasswordRulesErrorContext<'a> {
171 fn from_tag(input: &'a str, tag: &'static str) -> Self {
172 Self {
173 expectations: vec![ExpectedContext {
174 input_tail: input,
175 expected: Expected::Tag(tag),
176 }],
177 }
178 }
179}
180
181impl<'a> FromExternalError<&'a str, std::num::ParseIntError> for PasswordRulesErrorContext<'a> {
182 fn from_external_error(input: &'a str, kind: ErrorKind, _: std::num::ParseIntError) -> Self {
183 Self::from_error_kind(input, kind)
184 }
185}
186
187#[derive(Debug, Clone)]
189pub struct PasswordRulesError {
190 pub expectations: Vec<ExpectedAt>,
193}
194
195impl PasswordRulesError {
196 pub(crate) fn empty() -> Self {
197 Self {
198 expectations: vec![],
199 }
200 }
201
202 pub fn to_string_pretty(&self, s: &str) -> Result<String, fmt::Error> {
224 let lint_base = PrettyLint::error(s).with_message("parsing failed");
225
226 Ok(match self.expectations.as_slice() {
227 [] => lint_base.with_inline_message("unknown error").to_string(),
228 [exp] => lint_base
229 .with_inline_message(&format!("expected {}", exp.expected))
230 .at(Span {
231 start: Position {
232 line: exp.line as usize,
233 col: exp.column as usize,
234 },
235 end: Position {
236 line: exp.line as usize,
237 col: exp.column as usize,
238 },
239 })
240 .to_string(),
241 expectations => {
242 let groups = expectations.iter().chunk_by(|exp| (exp.line, exp.column));
245 let mut lint_string = String::new();
246
247 groups.into_iter().try_for_each(|((line, column), group)| {
248 let mut inline_message = String::from("expected one of ");
249
250 group
251 .with_position()
252 .try_for_each(|positioned_exp| match positioned_exp {
253 (Only, exp) => write!(inline_message, "{}", exp.expected),
254 (First, exp) | (Middle, exp) => {
255 write!(inline_message, "{}, ", exp.expected)
256 }
257 (Last, exp) => write!(inline_message, "or {}", exp.expected),
258 })?;
259
260 let lint = PrettyLint::error(s)
261 .with_message("parsing failed")
262 .with_inline_message(&inline_message)
263 .at(Span {
264 start: Position {
265 line: line as usize,
266 col: column as usize,
267 },
268 end: Position {
269 line: line as usize,
270 col: column as usize,
271 },
272 });
273
274 write!(lint_string, "{lint}")
275 })?;
276
277 lint_string
278 }
279 })
280 }
281}
282
283impl Display for PasswordRulesError {
284 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
285 match self.expectations.as_slice() {
286 [] => write!(f, "unknown error"),
287 [exp] => write!(
288 f,
289 "expected {} at {}:{}",
290 exp.expected, exp.line, exp.column
291 ),
292 expectations => {
293 writeln!(f, "expected one of:")?;
296
297 let groups = expectations.iter().chunk_by(|exp| (exp.line, exp.column));
298
299 groups.into_iter().try_for_each(|((line, column), group)| {
300 write!(f, " ")?;
301
302 group
303 .with_position()
304 .try_for_each(|positioned_exp| match positioned_exp {
305 (Only, exp) => write!(f, "{}", exp.expected),
306 (First, exp) | (Middle, exp) => write!(f, "{}, ", exp.expected),
307 (Last, exp) => write!(f, "or {}", exp.expected),
308 })?;
309
310 writeln!(f, " at {line}:{column}")
311 })
312 }
313 }
314 }
315}
316
317impl Error for PasswordRulesError {}