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::span::{FileSpan, LintSpan};
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash)]
11pub enum SourceFile {
12 Stdin,
13 File(String),
14}
15
16impl SourceFile {
17 #[must_use]
18 pub const fn as_str(&self) -> &str {
19 match self {
20 Self::Stdin => "<stdin>",
21 Self::File(path) => path.as_str(),
22 }
23 }
24
25 #[must_use]
26 pub fn as_path(&self) -> Option<&Path> {
27 match self {
28 Self::Stdin => None,
29 Self::File(path) => Some(Path::new(path)),
30 }
31 }
32
33 #[must_use]
34 pub const fn is_stdin(&self) -> bool {
35 matches!(self, Self::Stdin)
36 }
37}
38
39impl fmt::Display for SourceFile {
40 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
41 write!(f, "{}", self.as_str())
42 }
43}
44
45impl From<&str> for SourceFile {
46 fn from(s: &str) -> Self {
47 Self::File(s.to_string())
48 }
49}
50
51impl From<String> for SourceFile {
52 fn from(s: String) -> Self {
53 Self::File(s)
54 }
55}
56
57impl From<&Path> for SourceFile {
58 fn from(p: &Path) -> Self {
59 Self::File(p.to_string_lossy().to_string())
60 }
61}
62
63#[derive(Debug, Clone)]
69pub struct ExternalDetection {
70 pub file: String,
72 pub source: String,
74 pub span: FileSpan,
76 pub message: String,
78 pub label: Option<String>,
80}
81
82impl ExternalDetection {
83 #[must_use]
84 pub fn new(
85 file: impl Into<String>,
86 source: impl Into<String>,
87 span: FileSpan,
88 message: impl Into<String>,
89 ) -> Self {
90 Self {
91 file: file.into(),
92 source: source.into(),
93 span,
94 message: message.into(),
95 label: None,
96 }
97 }
98
99 #[must_use]
100 pub fn with_label(mut self, label: impl Into<String>) -> Self {
101 self.label = Some(label.into());
102 self
103 }
104}
105
106impl fmt::Display for ExternalDetection {
107 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108 write!(f, "{}", self.message)
109 }
110}
111
112impl Error for ExternalDetection {}
113
114impl Diagnostic for ExternalDetection {
115 fn severity(&self) -> Option<Severity> {
116 Some(Severity::Advice)
117 }
118
119 fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
120 let label = LabeledSpan::at(
121 self.span.start..self.span.end,
122 self.label.as_deref().unwrap_or("here"),
123 );
124 Some(Box::new(once(label)))
125 }
126}
127
128#[derive(Debug, Clone)]
134pub struct Detection {
135 pub message: Cow<'static, str>,
136 pub span: LintSpan,
137 pub primary_label: Option<Cow<'static, str>>,
138 pub extra_labels: Vec<(LintSpan, Option<String>)>,
139 pub external_detections: Vec<ExternalDetection>,
141}
142
143impl Detection {
144 #[must_use]
146 pub fn from_global_span(message: impl Into<Cow<'static, str>>, global_span: Span) -> Self {
147 Self {
148 message: message.into(),
149 span: LintSpan::from(global_span),
150 primary_label: None,
151 extra_labels: Vec::new(),
152 external_detections: Vec::new(),
153 }
154 }
155
156 #[must_use]
158 pub fn from_file_span(message: impl Into<Cow<'static, str>>, span: FileSpan) -> Self {
159 Self {
160 message: message.into(),
161 span: LintSpan::File(span),
162 primary_label: None,
163 extra_labels: Vec::new(),
164 external_detections: Vec::new(),
165 }
166 }
167
168 #[must_use]
170 pub fn with_external_detection(mut self, detection: ExternalDetection) -> Self {
171 self.external_detections.push(detection);
172 self
173 }
174
175 #[must_use]
176 pub fn with_primary_label(mut self, label: impl Into<Cow<'static, str>>) -> Self {
177 self.primary_label = Some(label.into());
178 self
179 }
180
181 #[must_use]
182 pub fn with_extra_label(mut self, label: impl Into<Cow<'static, str>>, span: Span) -> Self {
183 self.extra_labels
184 .push((LintSpan::from(span), Some(label.into().to_string())));
185 self
186 }
187
188 #[must_use]
189 pub fn with_extra_span(mut self, span: Span) -> Self {
190 self.extra_labels.push((LintSpan::from(span), None));
191 self
192 }
193}
194
195#[derive(Debug, Clone)]
202pub struct Violation {
203 pub rule_id: Option<Cow<'static, str>>,
204 pub lint_level: Severity,
205 pub message: Cow<'static, str>,
206 pub span: LintSpan,
207 pub primary_label: Option<Cow<'static, str>>,
208 pub extra_labels: Vec<(LintSpan, Option<String>)>,
209 pub long_description: Option<String>,
210 pub fix: Option<Fix>,
211 pub(crate) file: Option<SourceFile>,
212 pub(crate) source: Option<Cow<'static, str>>,
213 pub doc_url: Option<&'static str>,
214 pub short_description: Option<&'static str>,
216 pub diagnostic_tags: Vec<DiagnosticTag>,
218 pub external_detections: Vec<ExternalDetection>,
220}
221
222impl Violation {
223 pub(crate) fn from_detected(
224 detected: Detection,
225 fix: Option<Fix>,
226 long_description: impl Into<Option<&'static str>>,
227 ) -> Self {
228 Self {
229 rule_id: None,
230 lint_level: Severity::default(),
231 message: detected.message,
232 span: detected.span,
233 primary_label: detected.primary_label,
234 extra_labels: detected.extra_labels,
235 long_description: long_description.into().map(ToString::to_string),
236 fix,
237 file: None,
238 source: None,
239 doc_url: None,
240 short_description: None,
241 diagnostic_tags: Vec::new(),
242 external_detections: detected.external_detections,
243 }
244 }
245
246 pub(crate) fn set_rule_id(&mut self, rule_id: &'static str) {
248 self.rule_id = Some(Cow::Borrowed(rule_id));
249 }
250
251 pub(crate) const fn set_lint_level(&mut self, level: Severity) {
253 self.lint_level = level;
254 }
255
256 pub(crate) const fn set_doc_url(&mut self, url: Option<&'static str>) {
258 self.doc_url = url;
259 }
260
261 pub(crate) const fn set_short_description(&mut self, desc: &'static str) {
263 self.short_description = Some(desc);
264 }
265
266 pub(crate) fn set_diagnostic_tags(&mut self, tags: &[DiagnosticTag]) {
268 self.diagnostic_tags = tags.to_vec();
269 }
270
271 #[must_use]
273 pub fn file_span(&self) -> FileSpan {
274 self.span.file_span()
275 }
276
277 pub fn normalize_spans(&mut self, file_offset: usize) {
279 let file_span = self.span.to_file_span(file_offset);
281 self.span = LintSpan::File(file_span);
282
283 if let Some(fix) = &mut self.fix {
285 for replacement in &mut fix.replacements {
286 let file_span = replacement.span.to_file_span(file_offset);
287 replacement.span = LintSpan::File(file_span);
288 }
289 }
290
291 self.extra_labels = self
293 .extra_labels
294 .iter()
295 .map(|(span, label)| {
296 let file_span = span.to_file_span(file_offset);
297 (LintSpan::File(file_span), label.clone())
298 })
299 .collect();
300 }
301}
302
303impl fmt::Display for Violation {
304 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
305 write!(f, "{}", self.message)
306 }
307}
308
309impl Error for Violation {}
310
311impl Diagnostic for Violation {
312 fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
313 Some(Box::new(format!(
314 "{:?}({})",
315 self.lint_level,
316 self.rule_id.as_deref().unwrap_or("unknown")
317 )))
318 }
319
320 fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
321 self.long_description
322 .as_ref()
323 .map(|h| Box::new(h.clone()) as Box<dyn fmt::Display>)
324 }
325
326 fn url<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
327 self.doc_url
328 .map(|url| Box::new(url) as Box<dyn fmt::Display>)
329 }
330
331 fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
332 let file_span = self.file_span();
333 let span_range = file_span.start..file_span.end;
334 let primary = self.primary_label.as_ref().map_or_else(
335 || LabeledSpan::underline(span_range.clone()),
336 |label| LabeledSpan::new_primary_with_span(Some(label.to_string()), span_range.clone()),
337 );
338 let extras = self.extra_labels.iter().map(|(span, label)| {
339 let file_span = span.file_span();
340 LabeledSpan::new_with_span(label.clone(), file_span.start..file_span.end)
341 });
342 Some(Box::new(once(primary).chain(extras)))
343 }
344}
345
346#[derive(Debug, Clone)]
348pub struct Fix {
349 pub explanation: Cow<'static, str>,
352
353 pub replacements: Vec<Replacement>,
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}