1use std::{borrow::Cow, error::Error, fmt, iter::once, path::Path, string::ToString};
2
3use lsp_types::DiagnosticTag;
4use miette::{Diagnostic, LabeledSpan, Severity};
5use nu_protocol::Span;
6
7use crate::{
8 config::LintLevel,
9 span::{FileSpan, LintSpan},
10};
11
12#[derive(Debug, Clone, PartialEq, Eq, Hash)]
14pub enum SourceFile {
15 Stdin,
16 File(String),
17}
18
19impl SourceFile {
20 #[must_use]
21 pub const fn as_str(&self) -> &str {
22 match self {
23 Self::Stdin => "<stdin>",
24 Self::File(path) => path.as_str(),
25 }
26 }
27
28 #[must_use]
29 pub fn as_path(&self) -> Option<&Path> {
30 match self {
31 Self::Stdin => None,
32 Self::File(path) => Some(Path::new(path)),
33 }
34 }
35
36 #[must_use]
37 pub const fn is_stdin(&self) -> bool {
38 matches!(self, Self::Stdin)
39 }
40}
41
42impl fmt::Display for SourceFile {
43 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
44 write!(f, "{}", self.as_str())
45 }
46}
47
48impl From<&str> for SourceFile {
49 fn from(s: &str) -> Self {
50 Self::File(s.to_string())
51 }
52}
53
54impl From<String> for SourceFile {
55 fn from(s: String) -> Self {
56 Self::File(s)
57 }
58}
59
60impl From<&Path> for SourceFile {
61 fn from(p: &Path) -> Self {
62 Self::File(p.to_string_lossy().to_string())
63 }
64}
65
66impl From<LintLevel> for Severity {
67 fn from(level: LintLevel) -> Self {
68 match level {
69 LintLevel::Error => Self::Error,
70 LintLevel::Warning => Self::Warning,
71 LintLevel::Hint => Self::Advice,
72 }
73 }
74}
75
76#[derive(Debug, Clone)]
82pub struct ExternalDetection {
83 pub file: String,
85 pub source: String,
87 pub span: FileSpan,
89 pub message: String,
91 pub label: Option<String>,
93}
94
95impl ExternalDetection {
96 #[must_use]
97 pub fn new(
98 file: impl Into<String>,
99 source: impl Into<String>,
100 span: FileSpan,
101 message: impl Into<String>,
102 ) -> Self {
103 Self {
104 file: file.into(),
105 source: source.into(),
106 span,
107 message: message.into(),
108 label: None,
109 }
110 }
111
112 #[must_use]
113 pub fn with_label(mut self, label: impl Into<String>) -> Self {
114 self.label = Some(label.into());
115 self
116 }
117}
118
119#[derive(Debug, Clone)]
125pub struct Detection {
126 pub message: Cow<'static, str>,
127 pub span: LintSpan,
128 pub primary_label: Option<Cow<'static, str>>,
129 pub extra_labels: Vec<(LintSpan, Option<String>)>,
130 pub external_detections: Vec<ExternalDetection>,
132}
133
134impl Detection {
135 #[must_use]
137 pub fn from_global_span(message: impl Into<Cow<'static, str>>, global_span: Span) -> Self {
138 Self {
139 message: message.into(),
140 span: LintSpan::from(global_span),
141 primary_label: None,
142 extra_labels: Vec::new(),
143 external_detections: Vec::new(),
144 }
145 }
146
147 #[must_use]
149 pub fn from_file_span(message: impl Into<Cow<'static, str>>, span: FileSpan) -> Self {
150 Self {
151 message: message.into(),
152 span: LintSpan::File(span),
153 primary_label: None,
154 extra_labels: Vec::new(),
155 external_detections: Vec::new(),
156 }
157 }
158
159 #[must_use]
161 pub fn with_external_detection(mut self, detection: ExternalDetection) -> Self {
162 self.external_detections.push(detection);
163 self
164 }
165
166 #[must_use]
167 pub fn with_primary_label(mut self, label: impl Into<Cow<'static, str>>) -> Self {
168 self.primary_label = Some(label.into());
169 self
170 }
171
172 #[must_use]
173 pub fn with_extra_label(mut self, label: impl Into<Cow<'static, str>>, span: Span) -> Self {
174 self.extra_labels
175 .push((LintSpan::from(span), Some(label.into().to_string())));
176 self
177 }
178
179 #[must_use]
180 pub fn with_extra_span(mut self, span: Span) -> Self {
181 self.extra_labels.push((LintSpan::from(span), None));
182 self
183 }
184}
185
186#[derive(Debug, Clone)]
193pub struct Violation {
194 pub rule_id: Option<Cow<'static, str>>,
195 pub lint_level: LintLevel,
196 pub message: Cow<'static, str>,
197 pub span: LintSpan,
198 pub primary_label: Option<Cow<'static, str>>,
199 pub extra_labels: Vec<(LintSpan, Option<String>)>,
200 pub long_description: Option<String>,
201 pub fix: Option<Fix>,
202 pub(crate) file: Option<SourceFile>,
203 pub(crate) source: Option<Cow<'static, str>>,
204 pub doc_url: Option<&'static str>,
205 pub short_description: Option<&'static str>,
207 pub diagnostic_tags: Vec<DiagnosticTag>,
209 pub external_detections: Vec<ExternalDetection>,
211}
212
213impl Violation {
214 pub(crate) fn from_detected(
215 detected: Detection,
216 fix: Option<Fix>,
217 long_description: impl Into<Option<&'static str>>,
218 ) -> Self {
219 Self {
220 rule_id: None,
221 lint_level: LintLevel::default(),
222 message: detected.message,
223 span: detected.span,
224 primary_label: detected.primary_label,
225 extra_labels: detected.extra_labels,
226 long_description: long_description.into().map(ToString::to_string),
227 fix,
228 file: None,
229 source: None,
230 doc_url: None,
231 short_description: None,
232 diagnostic_tags: Vec::new(),
233 external_detections: detected.external_detections,
234 }
235 }
236
237 pub(crate) fn set_rule_id(&mut self, rule_id: &'static str) {
239 self.rule_id = Some(Cow::Borrowed(rule_id));
240 }
241
242 pub(crate) const fn set_lint_level(&mut self, level: LintLevel) {
244 self.lint_level = level;
245 }
246
247 pub(crate) const fn set_doc_url(&mut self, url: Option<&'static str>) {
249 self.doc_url = url;
250 }
251
252 pub(crate) const fn set_short_description(&mut self, desc: &'static str) {
254 self.short_description = Some(desc);
255 }
256
257 pub(crate) fn set_diagnostic_tags(&mut self, tags: &[DiagnosticTag]) {
259 self.diagnostic_tags = tags.to_vec();
260 }
261
262 #[must_use]
264 pub fn file_span(&self) -> FileSpan {
265 self.span.file_span()
266 }
267
268 pub fn normalize_spans(&mut self, file_offset: usize) {
270 let file_span = self.span.to_file_span(file_offset);
272 self.span = LintSpan::File(file_span);
273
274 if let Some(fix) = &mut self.fix {
276 for replacement in &mut fix.replacements {
277 let file_span = replacement.span.to_file_span(file_offset);
278 replacement.span = LintSpan::File(file_span);
279 }
280 }
281
282 self.extra_labels = self
284 .extra_labels
285 .iter()
286 .map(|(span, label)| {
287 let file_span = span.to_file_span(file_offset);
288 (LintSpan::File(file_span), label.clone())
289 })
290 .collect();
291 }
292}
293
294impl fmt::Display for Violation {
295 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
296 write!(f, "{}", self.message)
297 }
298}
299
300impl Error for Violation {}
301
302impl Diagnostic for Violation {
303 fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
304 Some(Box::new(format!(
305 "{:?}({})",
306 self.lint_level,
307 self.rule_id.as_deref().unwrap_or("unknown")
308 )))
309 }
310
311 fn severity(&self) -> Option<Severity> {
312 Some(self.lint_level.into())
313 }
314
315 fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
316 self.long_description
317 .as_ref()
318 .map(|h| Box::new(h.clone()) as Box<dyn fmt::Display>)
319 }
320
321 fn url<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
322 self.doc_url
323 .map(|url| Box::new(url) as Box<dyn fmt::Display>)
324 }
325
326 fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
327 let file_span = self.file_span();
328 let span_range = file_span.start..file_span.end;
329 let primary = self.primary_label.as_ref().map_or_else(
330 || LabeledSpan::underline(span_range.clone()),
331 |label| LabeledSpan::new_primary_with_span(Some(label.to_string()), span_range.clone()),
332 );
333 let extras = self.extra_labels.iter().map(|(span, label)| {
334 let file_span = span.file_span();
335 LabeledSpan::new_with_span(label.clone(), file_span.start..file_span.end)
336 });
337 Some(Box::new(once(primary).chain(extras)))
338 }
339}
340
341#[derive(Debug, Clone)]
343pub struct Fix {
344 pub explanation: Cow<'static, str>,
347
348 pub replacements: Vec<Replacement>,
350}
351
352#[derive(Debug, Clone)]
361pub struct Replacement {
362 pub span: LintSpan,
364
365 pub replacement_text: Cow<'static, str>,
367}
368
369impl Replacement {
370 #[must_use]
372 pub fn new(span: Span, replacement_text: impl Into<Cow<'static, str>>) -> Self {
373 Self {
374 span: LintSpan::from(span),
375 replacement_text: replacement_text.into(),
376 }
377 }
378
379 #[must_use]
381 pub fn with_file_span(span: FileSpan, replacement_text: impl Into<Cow<'static, str>>) -> Self {
382 Self {
383 span: LintSpan::File(span),
384 replacement_text: replacement_text.into(),
385 }
386 }
387
388 #[must_use]
390 pub fn file_span(&self) -> FileSpan {
391 match self.span {
392 LintSpan::File(f) => f,
393 LintSpan::Global(_) => panic!("Span not normalized - call normalize_spans first"),
394 }
395 }
396}