1use std::{borrow::Cow, error::Error, fmt, path::Path};
2
3use miette::{Diagnostic, LabeledSpan, Severity};
4use nu_protocol::Span;
5
6use crate::{
7 config::LintLevel,
8 span::{FileSpan, LintSpan},
9};
10
11#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13pub enum SourceFile {
14 Stdin,
15 File(String),
16}
17
18impl SourceFile {
19 #[must_use]
20 pub const fn as_str(&self) -> &str {
21 match self {
22 Self::Stdin => "<stdin>",
23 Self::File(path) => path.as_str(),
24 }
25 }
26
27 #[must_use]
28 pub fn as_path(&self) -> Option<&Path> {
29 match self {
30 Self::Stdin => None,
31 Self::File(path) => Some(Path::new(path)),
32 }
33 }
34
35 #[must_use]
36 pub const fn is_stdin(&self) -> bool {
37 matches!(self, Self::Stdin)
38 }
39}
40
41impl fmt::Display for SourceFile {
42 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
43 write!(f, "{}", self.as_str())
44 }
45}
46
47impl From<&str> for SourceFile {
48 fn from(s: &str) -> Self {
49 Self::File(s.to_string())
50 }
51}
52
53impl From<String> for SourceFile {
54 fn from(s: String) -> Self {
55 Self::File(s)
56 }
57}
58
59impl From<&Path> for SourceFile {
60 fn from(p: &Path) -> Self {
61 Self::File(p.to_string_lossy().to_string())
62 }
63}
64
65impl From<LintLevel> for Severity {
66 fn from(level: LintLevel) -> Self {
67 match level {
68 LintLevel::Error => Self::Error,
69 LintLevel::Warning => Self::Warning,
70 LintLevel::Hint => Self::Advice,
71 }
72 }
73}
74
75#[derive(Debug, Clone)]
81pub struct DetectedViolation {
82 pub message: Cow<'static, str>,
83 pub span: LintSpan,
84 pub primary_label: Option<Cow<'static, str>>,
85 pub extra_labels: Vec<(LintSpan, Option<String>)>,
86 pub help: Option<Cow<'static, str>>,
87 pub notes: Vec<Cow<'static, str>>,
88}
89
90impl DetectedViolation {
91 #[must_use]
93 pub fn from_global_span(message: impl Into<Cow<'static, str>>, global_span: Span) -> Self {
94 Self {
95 message: message.into(),
96 span: LintSpan::from(global_span),
97 primary_label: None,
98 extra_labels: Vec::new(),
99 help: None,
100 notes: Vec::new(),
101 }
102 }
103
104 #[must_use]
106 pub fn from_file_span(message: impl Into<Cow<'static, str>>, span: FileSpan) -> Self {
107 Self {
108 message: message.into(),
109 span: LintSpan::File(span),
110 primary_label: None,
111 extra_labels: Vec::new(),
112 help: None,
113 notes: Vec::new(),
114 }
115 }
116
117 #[must_use]
118 pub fn with_help(mut self, help: impl Into<Cow<'static, str>>) -> Self {
119 self.help = Some(help.into());
120 self
121 }
122
123 #[must_use]
124 pub fn with_primary_label(mut self, label: impl Into<Cow<'static, str>>) -> Self {
125 self.primary_label = Some(label.into());
126 self
127 }
128
129 #[must_use]
130 pub fn with_extra_label(mut self, label: impl Into<Cow<'static, str>>, span: Span) -> Self {
131 self.extra_labels
132 .push((LintSpan::from(span), Some(label.into().to_string())));
133 self
134 }
135
136 #[must_use]
137 pub fn with_extra_span(mut self, span: Span) -> Self {
138 self.extra_labels.push((LintSpan::from(span), None));
139 self
140 }
141
142 #[must_use]
143 pub fn with_notes<I, S>(mut self, notes: I) -> Self
144 where
145 I: IntoIterator<Item = S>,
146 S: Into<Cow<'static, str>>,
147 {
148 self.notes = notes.into_iter().map(Into::into).collect();
149 self
150 }
151
152 #[must_use]
153 pub fn with_note(mut self, note: impl Into<Cow<'static, str>>) -> Self {
154 self.notes.push(note.into());
155 self
156 }
157}
158
159#[derive(Debug, Clone)]
166pub struct Violation {
167 pub rule_id: Option<Cow<'static, str>>,
168 pub lint_level: LintLevel,
169 pub message: Cow<'static, str>,
170 pub span: LintSpan,
171 pub primary_label: Option<Cow<'static, str>>,
172 pub extra_labels: Vec<(LintSpan, Option<String>)>,
173 pub help: Option<Cow<'static, str>>,
174 pub notes: Vec<Cow<'static, str>>,
175 pub fix: Option<Fix>,
176 pub(crate) file: Option<SourceFile>,
177 pub(crate) source: Option<Cow<'static, str>>,
178 pub doc_url: Option<&'static str>,
179}
180
181impl Violation {
182 pub(crate) fn from_detected(detected: DetectedViolation, fix: Option<Fix>) -> Self {
183 Self {
184 rule_id: None,
185 lint_level: LintLevel::default(),
186 message: detected.message,
187 span: detected.span,
188 primary_label: detected.primary_label,
189 extra_labels: detected.extra_labels,
190 help: detected.help,
191 notes: detected.notes,
192 fix,
193 file: None,
194 source: None,
195 doc_url: None,
196 }
197 }
198
199 pub(crate) fn set_rule_id(&mut self, rule_id: &'static str) {
201 self.rule_id = Some(Cow::Borrowed(rule_id));
202 }
203
204 pub(crate) const fn set_lint_level(&mut self, level: LintLevel) {
206 self.lint_level = level;
207 }
208
209 pub(crate) const fn set_doc_url(&mut self, url: Option<&'static str>) {
211 self.doc_url = url;
212 }
213
214 #[must_use]
216 pub fn file_span(&self) -> FileSpan {
217 self.span.file_span()
218 }
219
220 pub fn extra_labels_file_spans(&self) -> impl Iterator<Item = (FileSpan, Option<&String>)> {
222 self.extra_labels
223 .iter()
224 .map(|(span, label)| (span.file_span(), label.as_ref()))
225 }
226
227 pub fn normalize_spans(&mut self, file_offset: usize) {
229 let file_span = self.span.to_file_span(file_offset);
231 self.span = LintSpan::File(file_span);
232
233 if let Some(fix) = &mut self.fix {
235 for replacement in &mut fix.replacements {
236 let file_span = replacement.span.to_file_span(file_offset);
237 replacement.span = LintSpan::File(file_span);
238 }
239 }
240
241 self.extra_labels = self
243 .extra_labels
244 .iter()
245 .map(|(span, label)| {
246 let file_span = span.to_file_span(file_offset);
247 (LintSpan::File(file_span), label.clone())
248 })
249 .collect();
250 }
251}
252
253impl fmt::Display for Violation {
254 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255 write!(f, "{}", self.message)
256 }
257}
258
259impl Error for Violation {}
260
261impl Diagnostic for Violation {
262 fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
263 Some(Box::new(format!(
264 "{:?}({})",
265 self.lint_level,
266 self.rule_id.as_deref().unwrap_or("unknown")
267 )))
268 }
269
270 fn severity(&self) -> Option<Severity> {
271 Some(self.lint_level.into())
272 }
273
274 fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
275 self.help
276 .as_ref()
277 .map(|h| Box::new(h.clone()) as Box<dyn fmt::Display>)
278 }
279
280 fn url<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
281 self.doc_url
282 .map(|url| Box::new(url) as Box<dyn fmt::Display>)
283 }
284
285 fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
286 let file_span = self.file_span();
287 let span_range = file_span.start..file_span.end;
288 let primary = self.primary_label.as_ref().map_or_else(
289 || LabeledSpan::underline(span_range.clone()),
290 |label| LabeledSpan::new_primary_with_span(Some(label.to_string()), span_range.clone()),
291 );
292 let extras = self.extra_labels.iter().map(|(span, label)| {
293 let file_span = span.file_span();
294 LabeledSpan::new_with_span(label.clone(), file_span.start..file_span.end)
295 });
296 Some(Box::new([primary].into_iter().chain(extras)))
297 }
298}
299
300#[derive(Debug, Clone)]
302pub struct Fix {
303 pub explanation: Cow<'static, str>,
306
307 pub replacements: Vec<Replacement>,
309}
310
311impl Fix {
312 #[must_use]
314 pub fn with_explanation(
315 explanation: impl Into<Cow<'static, str>>,
316 replacements: Vec<Replacement>,
317 ) -> Self {
318 Self {
319 explanation: explanation.into(),
320 replacements,
321 }
322 }
323}
324
325#[derive(Debug, Clone)]
334pub struct Replacement {
335 pub span: LintSpan,
337
338 pub replacement_text: Cow<'static, str>,
340}
341
342impl Replacement {
343 #[must_use]
345 pub fn new(span: Span, replacement_text: impl Into<Cow<'static, str>>) -> Self {
346 Self {
347 span: LintSpan::from(span),
348 replacement_text: replacement_text.into(),
349 }
350 }
351
352 #[must_use]
354 pub fn with_file_span(span: FileSpan, replacement_text: impl Into<Cow<'static, str>>) -> Self {
355 Self {
356 span: LintSpan::File(span),
357 replacement_text: replacement_text.into(),
358 }
359 }
360
361 #[must_use]
363 pub fn file_span(&self) -> FileSpan {
364 match self.span {
365 LintSpan::File(f) => f,
366 LintSpan::Global(_) => panic!("Span not normalized - call normalize_spans first"),
367 }
368 }
369}