1use std::{borrow::Cow, error::Error, fmt, iter::once, path::Path, string::ToString};
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 ExternalDetection {
82 pub file: String,
84 pub source: String,
86 pub span: FileSpan,
88 pub message: String,
90 pub label: Option<String>,
92}
93
94impl ExternalDetection {
95 #[must_use]
96 pub fn new(
97 file: impl Into<String>,
98 source: impl Into<String>,
99 span: FileSpan,
100 message: impl Into<String>,
101 ) -> Self {
102 Self {
103 file: file.into(),
104 source: source.into(),
105 span,
106 message: message.into(),
107 label: None,
108 }
109 }
110
111 #[must_use]
112 pub fn with_label(mut self, label: impl Into<String>) -> Self {
113 self.label = Some(label.into());
114 self
115 }
116}
117
118#[derive(Debug, Clone)]
124pub struct Detection {
125 pub message: Cow<'static, str>,
126 pub span: LintSpan,
127 pub primary_label: Option<Cow<'static, str>>,
128 pub extra_labels: Vec<(LintSpan, Option<String>)>,
129 pub external_detections: Vec<ExternalDetection>,
131}
132
133impl Detection {
134 #[must_use]
136 pub fn from_global_span(message: impl Into<Cow<'static, str>>, global_span: Span) -> Self {
137 Self {
138 message: message.into(),
139 span: LintSpan::from(global_span),
140 primary_label: None,
141 extra_labels: Vec::new(),
142 external_detections: Vec::new(),
143 }
144 }
145
146 #[must_use]
148 pub fn from_file_span(message: impl Into<Cow<'static, str>>, span: FileSpan) -> Self {
149 Self {
150 message: message.into(),
151 span: LintSpan::File(span),
152 primary_label: None,
153 extra_labels: Vec::new(),
154 external_detections: Vec::new(),
155 }
156 }
157
158 #[must_use]
160 pub fn with_external_detection(mut self, detection: ExternalDetection) -> Self {
161 self.external_detections.push(detection);
162 self
163 }
164
165 #[must_use]
166 pub fn with_primary_label(mut self, label: impl Into<Cow<'static, str>>) -> Self {
167 self.primary_label = Some(label.into());
168 self
169 }
170
171 #[must_use]
172 pub fn with_extra_label(mut self, label: impl Into<Cow<'static, str>>, span: Span) -> Self {
173 self.extra_labels
174 .push((LintSpan::from(span), Some(label.into().to_string())));
175 self
176 }
177
178 #[must_use]
179 pub fn with_extra_span(mut self, span: Span) -> Self {
180 self.extra_labels.push((LintSpan::from(span), None));
181 self
182 }
183}
184
185#[derive(Debug, Clone)]
192pub struct Violation {
193 pub rule_id: Option<Cow<'static, str>>,
194 pub lint_level: LintLevel,
195 pub message: Cow<'static, str>,
196 pub span: LintSpan,
197 pub primary_label: Option<Cow<'static, str>>,
198 pub extra_labels: Vec<(LintSpan, Option<String>)>,
199 pub long_description: Option<String>,
200 pub fix: Option<Fix>,
201 pub(crate) file: Option<SourceFile>,
202 pub(crate) source: Option<Cow<'static, str>>,
203 pub doc_url: Option<&'static str>,
204 pub external_detections: Vec<ExternalDetection>,
206}
207
208impl Violation {
209 pub(crate) fn from_detected(
210 detected: Detection,
211 fix: Option<Fix>,
212 long_description: impl Into<Option<&'static str>>,
213 ) -> Self {
214 Self {
215 rule_id: None,
216 lint_level: LintLevel::default(),
217 message: detected.message,
218 span: detected.span,
219 primary_label: detected.primary_label,
220 extra_labels: detected.extra_labels,
221 long_description: long_description.into().map(ToString::to_string),
222 fix,
223 file: None,
224 source: None,
225 doc_url: None,
226 external_detections: detected.external_detections,
227 }
228 }
229
230 pub(crate) fn set_rule_id(&mut self, rule_id: &'static str) {
232 self.rule_id = Some(Cow::Borrowed(rule_id));
233 }
234
235 pub(crate) const fn set_lint_level(&mut self, level: LintLevel) {
237 self.lint_level = level;
238 }
239
240 pub(crate) const fn set_doc_url(&mut self, url: Option<&'static str>) {
242 self.doc_url = url;
243 }
244
245 #[must_use]
247 pub fn file_span(&self) -> FileSpan {
248 self.span.file_span()
249 }
250
251 pub fn normalize_spans(&mut self, file_offset: usize) {
253 let file_span = self.span.to_file_span(file_offset);
255 self.span = LintSpan::File(file_span);
256
257 if let Some(fix) = &mut self.fix {
259 for replacement in &mut fix.replacements {
260 let file_span = replacement.span.to_file_span(file_offset);
261 replacement.span = LintSpan::File(file_span);
262 }
263 }
264
265 self.extra_labels = self
267 .extra_labels
268 .iter()
269 .map(|(span, label)| {
270 let file_span = span.to_file_span(file_offset);
271 (LintSpan::File(file_span), label.clone())
272 })
273 .collect();
274 }
275}
276
277impl fmt::Display for Violation {
278 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
279 write!(f, "{}", self.message)
280 }
281}
282
283impl Error for Violation {}
284
285impl Diagnostic for Violation {
286 fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
287 Some(Box::new(format!(
288 "{:?}({})",
289 self.lint_level,
290 self.rule_id.as_deref().unwrap_or("unknown")
291 )))
292 }
293
294 fn severity(&self) -> Option<Severity> {
295 Some(self.lint_level.into())
296 }
297
298 fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
299 self.long_description
300 .as_ref()
301 .map(|h| Box::new(h.clone()) as Box<dyn fmt::Display>)
302 }
303
304 fn url<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
305 self.doc_url
306 .map(|url| Box::new(url) as Box<dyn fmt::Display>)
307 }
308
309 fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
310 let file_span = self.file_span();
311 let span_range = file_span.start..file_span.end;
312 let primary = self.primary_label.as_ref().map_or_else(
313 || LabeledSpan::underline(span_range.clone()),
314 |label| LabeledSpan::new_primary_with_span(Some(label.to_string()), span_range.clone()),
315 );
316 let extras = self.extra_labels.iter().map(|(span, label)| {
317 let file_span = span.file_span();
318 LabeledSpan::new_with_span(label.clone(), file_span.start..file_span.end)
319 });
320 Some(Box::new(once(primary).chain(extras)))
321 }
322}
323
324#[derive(Debug, Clone)]
326pub struct Fix {
327 pub explanation: Cow<'static, str>,
330
331 pub replacements: Vec<Replacement>,
333}
334
335impl Fix {
336 #[must_use]
338 pub fn with_explanation(
339 explanation: impl Into<Cow<'static, str>>,
340 replacements: Vec<Replacement>,
341 ) -> Self {
342 Self {
343 explanation: explanation.into(),
344 replacements,
345 }
346 }
347}
348
349#[derive(Debug, Clone)]
358pub struct Replacement {
359 pub span: LintSpan,
361
362 pub replacement_text: Cow<'static, str>,
364}
365
366impl Replacement {
367 #[must_use]
369 pub fn new(span: Span, replacement_text: impl Into<Cow<'static, str>>) -> Self {
370 Self {
371 span: LintSpan::from(span),
372 replacement_text: replacement_text.into(),
373 }
374 }
375
376 #[must_use]
378 pub fn with_file_span(span: FileSpan, replacement_text: impl Into<Cow<'static, str>>) -> Self {
379 Self {
380 span: LintSpan::File(span),
381 replacement_text: replacement_text.into(),
382 }
383 }
384
385 #[must_use]
387 pub fn file_span(&self) -> FileSpan {
388 match self.span {
389 LintSpan::File(f) => f,
390 LintSpan::Global(_) => panic!("Span not normalized - call normalize_spans first"),
391 }
392 }
393}