1use colored::Colorize;
22
23pub use colored;
25
26#[derive(Debug, Default)]
28pub struct Position {
29 pub line: usize,
30 pub col: usize,
31}
32
33impl Position {
34 pub fn new(line: usize, col: usize) -> Self {
35 Self { col, line }
36 }
37}
38
39#[derive(Debug, Default)]
41pub struct Span {
42 pub start: Position,
43 pub end: Position,
44}
45
46impl Span {
47 pub fn new(start: Position, end: Position) -> Self {
48 Self { start, end }
49 }
50
51 pub fn range(start: (usize, usize), end: (usize, usize)) -> Self {
52 Self {
53 start: Position::new(start.0, start.1),
54 end: Position::new(end.0, end.1),
55 }
56 }
57}
58
59#[derive(Debug)]
61pub enum Severity {
62 Error,
63 Warning,
64 Info,
65 Success,
66}
67
68#[derive(Debug)]
70pub struct PrettyLint<'l> {
71 source: &'l str,
72 severity: Severity,
73 lint_code: Option<&'l str>,
74 file_path: Option<&'l str>,
75 span: Span,
76 message: Option<&'l str>,
77 inline_message: Option<&'l str>,
78 notes: &'l [&'l str],
79
80 is_additional: bool,
81
82 additional_lints: Vec<PrettyLint<'l>>,
83}
84
85impl<'l> PrettyLint<'l> {
86 pub fn new(source: &'l str) -> Self {
87 PrettyLint {
88 lint_code: None,
89 file_path: None,
90 inline_message: None,
91 message: None,
92 severity: Severity::Error,
93 span: Span::default(),
94 source,
95 notes: &[],
96 is_additional: false,
97 additional_lints: Vec::new(),
98 }
99 }
100
101 pub fn error(source: &'l str) -> Self {
102 PrettyLint::new(source).with_severity(Severity::Error)
103 }
104
105 pub fn warning(source: &'l str) -> Self {
106 PrettyLint::new(source).with_severity(Severity::Warning)
107 }
108
109 pub fn info(source: &'l str) -> Self {
110 PrettyLint::new(source).with_severity(Severity::Info)
111 }
112
113 pub fn success(source: &'l str) -> Self {
114 PrettyLint::new(source).with_severity(Severity::Success)
115 }
116
117 pub fn with_message(mut self, message: &'l str) -> Self {
118 self.message = Some(message);
119 self
120 }
121
122 pub fn with_code(mut self, code: &'l str) -> Self {
123 self.lint_code = Some(code);
124 self
125 }
126
127 pub fn with_severity(mut self, severity: Severity) -> Self {
128 self.severity = severity;
129 self
130 }
131
132 pub fn with_file_path(mut self, file_path: &'l str) -> Self {
133 self.file_path = Some(file_path);
134 self
135 }
136
137 pub fn with_inline_message(mut self, msg: &'l str) -> Self {
138 self.inline_message = Some(msg);
139 self
140 }
141
142 pub fn with_notes(mut self, notes: &'l [&'l str]) -> Self {
143 self.notes = notes;
144 self
145 }
146
147 pub fn at(mut self, span: Span) -> Self {
148 self.span = span;
149 self
150 }
151
152 pub fn and(mut self, mut lint: PrettyLint<'l>) -> Self {
153 lint.is_additional = true;
154 self.additional_lints.push(lint);
155 self
156 }
157}
158
159impl core::fmt::Display for PrettyLint<'_> {
160 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161 let multiline = self.span.start.line != self.span.end.line;
162
163 if let (Some(message), false) = (self.message, self.is_additional) {
164 match self.severity {
165 Severity::Error => "error".red().bold().fmt(f)?,
166 Severity::Warning => "warning".yellow().bold().fmt(f)?,
167 Severity::Info => "info".cyan().bold().fmt(f)?,
168 Severity::Success => "success".green().bold().fmt(f)?,
169 };
170
171 if let Some(code) = self.lint_code {
172 if !code.is_empty() {
173 match self.severity {
174 Severity::Error => {
175 "(".red().bold().fmt(f)?;
176 code.red().bold().fmt(f)?;
177 ")".red().bold().fmt(f)?;
178 }
179 Severity::Warning => {
180 "(".yellow().bold().fmt(f)?;
181 code.yellow().bold().fmt(f)?;
182 ")".yellow().bold().fmt(f)?;
183 }
184 Severity::Info => {
185 "(".cyan().bold().fmt(f)?;
186 code.cyan().bold().fmt(f)?;
187 ")".cyan().bold().fmt(f)?;
188 }
189 Severity::Success => {
190 "(".green().bold().fmt(f)?;
191 code.green().bold().fmt(f)?;
192 ")".green().bold().fmt(f)?;
193 }
194 };
195 }
196 }
197
198 ": ".bold().fmt(f)?;
199 message.bold().fmt(f)?;
200 f.write_str("\n")?;
201 }
202
203 let mut line_digits = digits(usize::max(self.span.start.line, self.span.end.line));
204
205 if multiline {
206 line_digits = usize::max(3, line_digits);
207 }
208
209 if !self.additional_lints.is_empty() {
210 for add in &self.additional_lints {
211 line_digits = usize::max(line_digits, digits(add.span.start.line));
212 line_digits = usize::max(line_digits, digits(add.span.end.line));
213 }
214 }
215
216 let num_padding = " ".repeat(line_digits);
217 let padding_sep = " | ".blue().bold();
218
219 let padding_sep_span = match self.severity {
220 Severity::Error => format!("{}{}", " |".blue().bold(), "|".red()),
221 Severity::Warning => format!("{}{}", " |".blue().bold(), "|".yellow()),
222 Severity::Info => format!("{}{}", " |".blue().bold(), "|".cyan()),
223 Severity::Success => format!("{}{}", " |".blue().bold(), "|".green()),
224 };
225
226 f.write_str(&num_padding)?;
227 if self.is_additional {
228 "... ".blue().bold().fmt(f)?;
229 } else {
230 "--> ".blue().bold().fmt(f)?;
231 }
232 if let Some(fpath) = self.file_path {
233 if !fpath.is_empty() {
234 f.write_str(fpath)?;
235 f.write_str(":")?;
236 }
237 }
238 f.write_str(&format!("{}:{}", self.span.start.line, self.span.start.col))?;
239 f.write_str("\n")?;
240
241 f.write_str(&num_padding)?;
242 padding_sep.fmt(f)?;
243 f.write_str("\n")?;
244
245 for (i, line) in self.source.split('\n').enumerate() {
246 let line_number = i + 1;
247
248 if line_number == self.span.start.line {
249 write!(
250 f,
251 "{: ^size$}",
252 line_number.to_string().blue().bold(),
253 size = line_digits
254 )?;
255
256 padding_sep.fmt(f)?;
257 f.write_str(line)?;
258 f.write_str("\n")?;
259 }
260
261 if line_number > self.span.end.line {
262 break;
263 }
264
265 if !multiline {
266 continue;
267 }
268
269 if line_number == self.span.start.line + 1 {
270 num_padding.fmt(f)?;
271 padding_sep.fmt(f)?;
272 for _ in 0..self.span.start.col.checked_sub(1).unwrap_or(0) {
273 match self.severity {
274 Severity::Error => {
275 "_".red().fmt(f)?;
276 }
277 Severity::Warning => {
278 "_".yellow().fmt(f)?;
279 }
280 Severity::Info => {
281 "_".cyan().fmt(f)?;
282 }
283 Severity::Success => {
284 "_".green().fmt(f)?;
285 }
286 }
287 }
288
289 match self.severity {
290 Severity::Error => {
291 "^".red().bold().fmt(f)?;
292 }
293 Severity::Warning => {
294 "^".yellow().bold().fmt(f)?;
295 }
296 Severity::Info => {
297 "^".cyan().bold().fmt(f)?;
298 }
299 Severity::Success => {
300 "^".green().bold().fmt(f)?;
301 }
302 }
303 }
304
305 if line_number == self.span.end.line {
306 if line_number - self.span.start.line > 1 {
307 f.write_str("\n")?;
308 ".".repeat(line_digits).bold().blue().fmt(f)?;
309 padding_sep_span.fmt(f)?;
310 f.write_str("\n")?;
311
312 num_padding.fmt(f)?;
313 padding_sep_span.fmt(f)?;
314 }
315 f.write_str("\n")?;
316
317 write!(
318 f,
319 "{: ^size$}",
320 line_number.to_string().blue().bold(),
321 size = line_digits
322 )?;
323 padding_sep_span.fmt(f)?;
324 f.write_str(line)?;
325 f.write_str("\n")?;
326
327 num_padding.fmt(f)?;
328 padding_sep_span.fmt(f)?;
329
330 for _ in 0..self.span.end.col.checked_sub(1).unwrap_or(0) {
331 match self.severity {
332 Severity::Error => {
333 "_".red().fmt(f)?;
334 }
335 Severity::Warning => {
336 "_".yellow().fmt(f)?;
337 }
338 Severity::Info => {
339 "_".cyan().fmt(f)?;
340 }
341 Severity::Success => {
342 "_".green().fmt(f)?;
343 }
344 }
345 }
346
347 match self.severity {
348 Severity::Error => {
349 "^".red().bold().fmt(f)?;
350 }
351 Severity::Warning => {
352 "^".yellow().bold().fmt(f)?;
353 }
354 Severity::Info => {
355 "^".cyan().bold().fmt(f)?;
356 }
357 Severity::Success => {
358 "^".green().bold().fmt(f)?;
359 }
360 }
361 }
362 }
363
364 if !multiline {
365 let error_len = self
366 .span
367 .end
368 .col
369 .checked_sub(self.span.start.col)
370 .unwrap_or(1);
371
372 f.write_str(&num_padding)?;
373 padding_sep.fmt(f)?;
374
375 for _ in 0..self.span.start.col.checked_sub(1).unwrap_or(0) {
376 f.write_str(" ")?;
377 }
378
379 for _ in 0..=error_len {
380 match self.severity {
381 Severity::Error => {
382 "^".red().bold().fmt(f)?;
383 }
384 Severity::Warning => {
385 "^".yellow().bold().fmt(f)?;
386 }
387 Severity::Info => {
388 "^".cyan().bold().fmt(f)?;
389 }
390 Severity::Success => {
391 "^".green().bold().fmt(f)?;
392 }
393 }
394 }
395 }
396
397 if let Some(msg) = self.inline_message {
398 f.write_str(" ")?;
399
400 match self.severity {
401 Severity::Error => {
402 msg.bold().red().fmt(f)?;
403 }
404 Severity::Warning => {
405 msg.bold().yellow().fmt(f)?;
406 }
407 Severity::Info => {
408 msg.bold().cyan().fmt(f)?;
409 }
410 Severity::Success => {
411 msg.bold().green().fmt(f)?;
412 }
413 }
414 }
415
416 if !self.additional_lints.is_empty() {
417 f.write_str("\n")?;
418 f.write_str(&num_padding)?;
419 padding_sep.fmt(f)?;
420 f.write_str("\n")?;
421 for add in &self.additional_lints {
422 add.fmt(f)?;
423 }
424 }
425
426 if !self.notes.is_empty() && !self.is_additional {
427 f.write_str("\n")?;
428 f.write_str(&num_padding)?;
429 padding_sep.fmt(f)?;
430 for note in self.notes {
431 f.write_str("\n")?;
432 num_padding.fmt(f)?;
433 " - ".bold().blue().fmt(f)?;
434 f.write_str(note)?;
435 }
436
437 for add in &self.additional_lints {
438 for note in add.notes {
439 f.write_str("\n")?;
440 num_padding.fmt(f)?;
441 " - ".bold().blue().fmt(f)?;
442 f.write_str(note)?;
443 }
444 }
445 }
446
447 Ok(())
448 }
449}
450
451fn digits(mut val: usize) -> usize {
454 if val == 0 {
455 return 1;
456 }
457
458 let mut count = 0;
459 while val != 0 {
460 val /= 10;
461 count += 1;
462 }
463
464 count
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470
471 #[test]
472 fn test_print() {
473 let src = r#"
474this is
475some text
476that is wrong
477"#;
478
479 let lint = PrettyLint::error(src)
480 .with_message("This is an error")
481 .with_file_path(file!())
482 .with_code("syntax")
483 .at(Span {
484 start: Position { line: 3, col: 1 },
485 end: Position { line: 3, col: 4 },
486 })
487 .with_inline_message("this is wrong")
488 .with_notes(&["This is a note", "This is another note"])
489 .and(
490 PrettyLint::info(src)
491 .with_inline_message("stuff is here")
492 .with_file_path(file!())
493 .at(Span {
494 start: Position { line: 2, col: 1 },
495 end: Position { line: 2, col: 4 },
496 }),
497 );
498
499 println!("{}", lint);
500 }
501}