1use std::{borrow::Cow, error::Error, fmt, iter::once, 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 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 help: Option<Cow<'static, str>>,
130 pub notes: Vec<Cow<'static, str>>,
131 pub external_detections: Vec<ExternalDetection>,
133}
134
135impl Detection {
136 #[must_use]
138 pub fn from_global_span(message: impl Into<Cow<'static, str>>, global_span: Span) -> Self {
139 Self {
140 message: message.into(),
141 span: LintSpan::from(global_span),
142 primary_label: None,
143 extra_labels: Vec::new(),
144 help: None,
145 notes: Vec::new(),
146 external_detections: Vec::new(),
147 }
148 }
149
150 #[must_use]
152 pub fn from_file_span(message: impl Into<Cow<'static, str>>, span: FileSpan) -> Self {
153 Self {
154 message: message.into(),
155 span: LintSpan::File(span),
156 primary_label: None,
157 extra_labels: Vec::new(),
158 help: None,
159 notes: Vec::new(),
160 external_detections: Vec::new(),
161 }
162 }
163
164 #[must_use]
166 pub fn with_external_detection(mut self, detection: ExternalDetection) -> Self {
167 self.external_detections.push(detection);
168 self
169 }
170
171 #[must_use]
172 pub fn with_help(mut self, help: impl Into<Cow<'static, str>>) -> Self {
173 self.help = Some(help.into());
174 self
175 }
176
177 #[must_use]
178 pub fn with_primary_label(mut self, label: impl Into<Cow<'static, str>>) -> Self {
179 self.primary_label = Some(label.into());
180 self
181 }
182
183 #[must_use]
184 pub fn with_extra_label(mut self, label: impl Into<Cow<'static, str>>, span: Span) -> Self {
185 self.extra_labels
186 .push((LintSpan::from(span), Some(label.into().to_string())));
187 self
188 }
189
190 #[must_use]
191 pub fn with_extra_span(mut self, span: Span) -> Self {
192 self.extra_labels.push((LintSpan::from(span), None));
193 self
194 }
195}
196
197#[derive(Debug, Clone)]
204pub struct Violation {
205 pub rule_id: Option<Cow<'static, str>>,
206 pub lint_level: LintLevel,
207 pub message: Cow<'static, str>,
208 pub span: LintSpan,
209 pub primary_label: Option<Cow<'static, str>>,
210 pub extra_labels: Vec<(LintSpan, Option<String>)>,
211 pub help: Option<Cow<'static, str>>,
212 pub fix: Option<Fix>,
213 pub(crate) file: Option<SourceFile>,
214 pub(crate) source: Option<Cow<'static, str>>,
215 pub doc_url: Option<&'static str>,
216 pub external_detections: Vec<ExternalDetection>,
218}
219
220impl Violation {
221 pub(crate) fn from_detected(detected: Detection, fix: Option<Fix>) -> Self {
222 Self {
223 rule_id: None,
224 lint_level: LintLevel::default(),
225 message: detected.message,
226 span: detected.span,
227 primary_label: detected.primary_label,
228 extra_labels: detected.extra_labels,
229 help: detected.help,
230 fix,
231 file: None,
232 source: None,
233 doc_url: None,
234 external_detections: detected.external_detections,
235 }
236 }
237
238 pub(crate) fn set_rule_id(&mut self, rule_id: &'static str) {
240 self.rule_id = Some(Cow::Borrowed(rule_id));
241 }
242
243 pub(crate) const fn set_lint_level(&mut self, level: LintLevel) {
245 self.lint_level = level;
246 }
247
248 pub(crate) const fn set_doc_url(&mut self, url: Option<&'static str>) {
250 self.doc_url = url;
251 }
252
253 #[must_use]
255 pub fn file_span(&self) -> FileSpan {
256 self.span.file_span()
257 }
258
259 pub fn normalize_spans(&mut self, file_offset: usize) {
261 let file_span = self.span.to_file_span(file_offset);
263 self.span = LintSpan::File(file_span);
264
265 if let Some(fix) = &mut self.fix {
267 for replacement in &mut fix.replacements {
268 let file_span = replacement.span.to_file_span(file_offset);
269 replacement.span = LintSpan::File(file_span);
270 }
271 }
272
273 self.extra_labels = self
275 .extra_labels
276 .iter()
277 .map(|(span, label)| {
278 let file_span = span.to_file_span(file_offset);
279 (LintSpan::File(file_span), label.clone())
280 })
281 .collect();
282 }
283}
284
285impl fmt::Display for Violation {
286 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
287 write!(f, "{}", self.message)
288 }
289}
290
291impl Error for Violation {}
292
293impl Diagnostic for Violation {
294 fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
295 Some(Box::new(format!(
296 "{:?}({})",
297 self.lint_level,
298 self.rule_id.as_deref().unwrap_or("unknown")
299 )))
300 }
301
302 fn severity(&self) -> Option<Severity> {
303 Some(self.lint_level.into())
304 }
305
306 fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
307 self.help
308 .as_ref()
309 .map(|h| Box::new(h.clone()) as Box<dyn fmt::Display>)
310 }
311
312 fn url<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
313 self.doc_url
314 .map(|url| Box::new(url) as Box<dyn fmt::Display>)
315 }
316
317 fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
318 let file_span = self.file_span();
319 let span_range = file_span.start..file_span.end;
320 let primary = self.primary_label.as_ref().map_or_else(
321 || LabeledSpan::underline(span_range.clone()),
322 |label| LabeledSpan::new_primary_with_span(Some(label.to_string()), span_range.clone()),
323 );
324 let extras = self.extra_labels.iter().map(|(span, label)| {
325 let file_span = span.file_span();
326 LabeledSpan::new_with_span(label.clone(), file_span.start..file_span.end)
327 });
328 Some(Box::new(once(primary).chain(extras)))
329 }
330}
331
332#[derive(Debug, Clone)]
334pub struct Fix {
335 pub explanation: Cow<'static, str>,
338
339 pub replacements: Vec<Replacement>,
341}
342
343impl Fix {
344 #[must_use]
346 pub fn with_explanation(
347 explanation: impl Into<Cow<'static, str>>,
348 replacements: Vec<Replacement>,
349 ) -> Self {
350 Self {
351 explanation: explanation.into(),
352 replacements,
353 }
354 }
355}
356
357#[derive(Debug, Clone)]
366pub struct Replacement {
367 pub span: LintSpan,
369
370 pub replacement_text: Cow<'static, str>,
372}
373
374impl Replacement {
375 #[must_use]
377 pub fn new(span: Span, replacement_text: impl Into<Cow<'static, str>>) -> Self {
378 Self {
379 span: LintSpan::from(span),
380 replacement_text: replacement_text.into(),
381 }
382 }
383
384 #[must_use]
386 pub fn with_file_span(span: FileSpan, replacement_text: impl Into<Cow<'static, str>>) -> Self {
387 Self {
388 span: LintSpan::File(span),
389 replacement_text: replacement_text.into(),
390 }
391 }
392
393 #[must_use]
395 pub fn file_span(&self) -> FileSpan {
396 match self.span {
397 LintSpan::File(f) => f,
398 LintSpan::Global(_) => panic!("Span not normalized - call normalize_spans first"),
399 }
400 }
401}