1use std::borrow::Cow;
2use std::collections::BTreeMap;
3use std::path::Path;
4
5use annotate_snippets::{
6 Annotation as AnnotateAnnotation, AnnotationKind, Group as AnnotateGroup,
7 Level as AnnotateLevel, Snippet as AnnotateSnippet,
8};
9use full::FullRenderer;
10use ruff_notebook::{Notebook, NotebookIndex};
11use ruff_source_file::{LineIndex, OneIndexed, SourceCode};
12use ruff_text_size::{TextLen, TextRange, TextSize};
13
14use crate::{
15 Db,
16 files::File,
17 source::{SourceText, line_index, source_text},
18};
19
20use super::{
21 Annotation, Diagnostic, DiagnosticFormat, DiagnosticSource, DisplayDiagnosticConfig,
22 SubDiagnostic, UnifiedFile,
23};
24
25use azure::AzureRenderer;
26use concise::ConciseRenderer;
27use github::GithubRenderer;
28use pylint::PylintRenderer;
29
30mod azure;
31mod concise;
32mod full;
33pub mod github;
34#[cfg(feature = "serde")]
35mod gitlab;
36#[cfg(feature = "serde")]
37mod json;
38#[cfg(feature = "serde")]
39mod json_lines;
40#[cfg(feature = "junit")]
41mod junit;
42mod pylint;
43#[cfg(feature = "serde")]
44mod rdjson;
45
46pub struct DisplayDiagnostic<'a> {
58 config: &'a DisplayDiagnosticConfig,
59 resolver: &'a dyn FileResolver,
60 diag: &'a Diagnostic,
61}
62
63impl<'a> DisplayDiagnostic<'a> {
64 pub(crate) fn new(
65 resolver: &'a dyn FileResolver,
66 config: &'a DisplayDiagnosticConfig,
67 diag: &'a Diagnostic,
68 ) -> DisplayDiagnostic<'a> {
69 DisplayDiagnostic {
70 config,
71 resolver,
72 diag,
73 }
74 }
75}
76
77impl std::fmt::Display for DisplayDiagnostic<'_> {
78 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
79 DisplayDiagnostics::new(self.resolver, self.config, std::slice::from_ref(self.diag)).fmt(f)
80 }
81}
82
83pub struct DisplayDiagnostics<'a> {
91 config: &'a DisplayDiagnosticConfig,
92 resolver: &'a dyn FileResolver,
93 diagnostics: &'a [Diagnostic],
94}
95
96impl<'a> DisplayDiagnostics<'a> {
97 pub fn new(
98 resolver: &'a dyn FileResolver,
99 config: &'a DisplayDiagnosticConfig,
100 diagnostics: &'a [Diagnostic],
101 ) -> DisplayDiagnostics<'a> {
102 DisplayDiagnostics {
103 config,
104 resolver,
105 diagnostics,
106 }
107 }
108}
109
110impl std::fmt::Display for DisplayDiagnostics<'_> {
111 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
112 match self.config.format {
113 DiagnosticFormat::Concise => {
114 ConciseRenderer::new(self.resolver, self.config).render(f, self.diagnostics)?;
115 }
116 DiagnosticFormat::Full => {
117 FullRenderer::new(self.resolver, self.config).render(f, self.diagnostics)?;
118 }
119 DiagnosticFormat::Azure => {
120 AzureRenderer::new(self.resolver, self.config).render(f, self.diagnostics)?;
121 }
122 #[cfg(feature = "serde")]
123 DiagnosticFormat::Json => {
124 json::JsonRenderer::new(self.resolver, self.config).render(f, self.diagnostics)?;
125 }
126 #[cfg(feature = "serde")]
127 DiagnosticFormat::JsonLines => {
128 json_lines::JsonLinesRenderer::new(self.resolver, self.config)
129 .render(f, self.diagnostics)?;
130 }
131 #[cfg(feature = "serde")]
132 DiagnosticFormat::Rdjson => {
133 rdjson::RdjsonRenderer::new(self.resolver, self.config)
134 .render(f, self.diagnostics)?;
135 }
136 DiagnosticFormat::Pylint => {
137 PylintRenderer::new(self.resolver, self.config).render(f, self.diagnostics)?;
138 }
139 #[cfg(feature = "junit")]
140 DiagnosticFormat::Junit => {
141 junit::JunitRenderer::new(self.resolver, self.config)
142 .render(f, self.diagnostics)?;
143 }
144 #[cfg(feature = "serde")]
145 DiagnosticFormat::Gitlab => {
146 gitlab::GitlabRenderer::new(self.resolver, self.config)
147 .render(f, self.diagnostics)?;
148 }
149 DiagnosticFormat::Github => {
150 GithubRenderer::new(self.resolver, self.config).render(f, self.diagnostics)?;
151 }
152 }
153
154 Ok(())
155 }
156}
157
158#[derive(Debug)]
171struct Resolved<'a> {
172 diagnostics: Vec<ResolvedDiagnostic<'a>>,
173}
174
175impl<'a> Resolved<'a> {
176 fn new(
178 resolver: &'a dyn FileResolver,
179 diag: &'a Diagnostic,
180 config: &DisplayDiagnosticConfig,
181 ) -> Resolved<'a> {
182 let mut diagnostics = vec![];
183 diagnostics.push(ResolvedDiagnostic::from_diagnostic(resolver, config, diag));
184 for sub in &diag.inner.subs {
185 diagnostics.push(ResolvedDiagnostic::from_sub_diagnostic(resolver, sub));
186 }
187 Resolved { diagnostics }
188 }
189
190 fn to_renderable(&self, config: &DisplayDiagnosticConfig) -> Renderable<'_> {
192 Renderable {
193 diagnostics: self
194 .diagnostics
195 .iter()
196 .map(|diag| diag.to_renderable(config))
197 .collect(),
198 }
199 }
200}
201
202#[derive(Debug)]
208struct ResolvedDiagnostic<'a> {
209 level: AnnotateLevel<'static>,
210 id: Option<String>,
211 documentation_url: Option<String>,
212 message: String,
213 annotations: Vec<ResolvedAnnotation<'a>>,
214 is_fixable: bool,
215 header_offset: usize,
216}
217
218impl<'a> ResolvedDiagnostic<'a> {
219 fn from_diagnostic(
221 resolver: &'a dyn FileResolver,
222 config: &DisplayDiagnosticConfig,
223 diag: &'a Diagnostic,
224 ) -> ResolvedDiagnostic<'a> {
225 let annotations: Vec<_> = diag
226 .inner
227 .annotations
228 .iter()
229 .filter_map(|ann| {
230 let path = ann
231 .span
232 .file
233 .relative_path(resolver)
234 .to_str()
235 .unwrap_or_else(|| ann.span.file.path(resolver));
236 let diagnostic_source = ann.span.file.diagnostic_source(resolver);
237 ResolvedAnnotation::new(path, &diagnostic_source, ann, resolver)
238 })
239 .collect();
240
241 let use_code = !config.preview || config.prefer_rule_codes;
242 let id = if use_code && let Some(code) = diag.secondary_code() {
243 code.to_string()
244 } else if config.hide_severity {
245 format!("{id}:", id = diag.id())
250 } else {
251 diag.id().to_string()
252 };
253
254 let level = diag.inner.severity.to_annotate();
255 let level = if config.hide_severity {
256 level.no_name()
257 } else {
258 level
259 };
260
261 ResolvedDiagnostic {
262 level,
263 id: Some(id),
264 documentation_url: diag.documentation_url().map(ToString::to_string),
265 message: diag.inner.message.as_str().to_string(),
266 annotations,
267 is_fixable: config.show_fix_status
268 && diag.has_applicable_fix(config.fix_applicability()),
269 header_offset: diag.inner.header_offset,
270 }
271 }
272
273 fn from_sub_diagnostic(
275 resolver: &'a dyn FileResolver,
276 diag: &'a SubDiagnostic,
277 ) -> ResolvedDiagnostic<'a> {
278 let annotations: Vec<_> = diag
279 .inner
280 .annotations
281 .iter()
282 .filter_map(|ann| {
283 let path = ann
284 .span
285 .file
286 .relative_path(resolver)
287 .to_str()
288 .unwrap_or_else(|| ann.span.file.path(resolver));
289 let diagnostic_source = ann.span.file.diagnostic_source(resolver);
290 ResolvedAnnotation::new(path, &diagnostic_source, ann, resolver)
291 })
292 .collect();
293 ResolvedDiagnostic {
294 level: diag.inner.severity.to_annotate(),
295 id: None,
296 documentation_url: None,
297 message: diag.inner.message.as_str().to_string(),
298 annotations,
299 is_fixable: false,
300 header_offset: 0,
301 }
302 }
303
304 fn to_renderable<'r>(&'r self, config: &DisplayDiagnosticConfig) -> RenderableDiagnostic<'r> {
309 let mut ann_by_path: BTreeMap<&'a str, Vec<&ResolvedAnnotation<'a>>> = BTreeMap::new();
310 for ann in &self.annotations {
311 ann_by_path.entry(ann.path).or_default().push(ann);
312 }
313 for anns in ann_by_path.values_mut() {
314 anns.sort_by_key(|ann1| ann1.range.start());
315 }
316
317 let merge_window = config.merge_window.max(config.context);
322
323 let mut snippet_by_path: BTreeMap<&'a str, Vec<Vec<&ResolvedAnnotation<'a>>>> =
324 BTreeMap::new();
325 for (path, anns) in ann_by_path {
326 let mut snippet = vec![];
327 for ann in anns {
328 let Some(prev) = snippet.last() else {
329 snippet.push(ann);
330 continue;
331 };
332
333 let prev_context_ends = context_after(
334 &prev.diagnostic_source.as_source_code(),
335 merge_window,
336 prev.line_end,
337 prev.notebook_index.as_ref(),
338 )
339 .get();
340 let this_context_begins = context_before(
341 &ann.diagnostic_source.as_source_code(),
342 merge_window,
343 ann.line_start,
344 ann.notebook_index.as_ref(),
345 )
346 .get();
347
348 let prev_cell_index = prev.notebook_index.as_ref().map(|notebook_index| {
352 let prev_end = prev
353 .diagnostic_source
354 .as_source_code()
355 .line_column(prev.range.end());
356 notebook_index.cell(prev_end.line).unwrap_or_default().get()
357 });
358 let this_cell_index = ann.notebook_index.as_ref().map(|notebook_index| {
359 let this_start = ann
360 .diagnostic_source
361 .as_source_code()
362 .line_column(ann.range.start());
363 notebook_index
364 .cell(this_start.line)
365 .unwrap_or_default()
366 .get()
367 });
368 let in_different_cells = prev_cell_index != this_cell_index;
369
370 if in_different_cells || this_context_begins.saturating_sub(prev_context_ends) > 1 {
380 snippet_by_path
381 .entry(path)
382 .or_default()
383 .push(std::mem::take(&mut snippet));
384 }
385 snippet.push(ann);
386 }
387 if !snippet.is_empty() {
388 snippet_by_path.entry(path).or_default().push(snippet);
389 }
390 }
391
392 let mut snippets_by_input = vec![];
393 for (path, snippets) in snippet_by_path {
394 snippets_by_input.push(RenderableSnippets::new(config.context, path, &snippets));
395 }
396 snippets_by_input
397 .sort_by(|snips1, snips2| snips1.has_primary.cmp(&snips2.has_primary).reverse());
398 RenderableDiagnostic {
399 level: self.level.clone(),
400 id: self.id.as_deref(),
401 documentation_url: self.documentation_url.as_deref(),
402 message: &self.message,
403 snippets_by_input,
404 is_fixable: self.is_fixable,
405 header_offset: self.header_offset,
406 }
407 }
408}
409
410#[derive(Debug)]
417struct ResolvedAnnotation<'a> {
418 path: &'a str,
419 diagnostic_source: DiagnosticSource,
420 range: TextRange,
421 line_start: OneIndexed,
422 line_end: OneIndexed,
423 message: Option<&'a str>,
424 is_primary: bool,
425 hide_snippet: bool,
426 notebook_index: Option<NotebookIndex>,
427}
428
429impl<'a> ResolvedAnnotation<'a> {
430 fn new(
436 path: &'a str,
437 diagnostic_source: &DiagnosticSource,
438 ann: &'a Annotation,
439 resolver: &'a dyn FileResolver,
440 ) -> Option<ResolvedAnnotation<'a>> {
441 let source = diagnostic_source.as_source_code();
442 let (range, line_start, line_end) = match (ann.span.range(), ann.message.is_some()) {
443 (None, _) => (
446 TextRange::empty(TextSize::new(0)),
447 OneIndexed::MIN,
448 OneIndexed::MIN,
449 ),
450 (Some(range), _) => {
451 let line_start = source.line_index(range.start());
452 let mut line_end = source.line_index(range.end());
453 if source.slice(range).ends_with(['\r', '\n']) {
460 line_end = line_end.saturating_sub(1).max(line_start);
461 }
462 (range, line_start, line_end)
463 }
464 };
465 Some(ResolvedAnnotation {
466 path,
467 diagnostic_source: diagnostic_source.clone(),
468 range,
469 line_start,
470 line_end,
471 message: ann.get_message(),
472 is_primary: ann.is_primary,
473 hide_snippet: ann.hide_snippet,
474 notebook_index: resolver.notebook_index(&ann.span.file),
475 })
476 }
477}
478
479#[derive(Debug)]
487struct Renderable<'r> {
488 diagnostics: Vec<RenderableDiagnostic<'r>>,
489}
490
491#[derive(Debug)]
493struct RenderableDiagnostic<'r> {
494 level: AnnotateLevel<'static>,
496 id: Option<&'r str>,
502 documentation_url: Option<&'r str>,
503 message: &'r str,
506 snippets_by_input: Vec<RenderableSnippets<'r>>,
510 is_fixable: bool,
514 header_offset: usize,
519}
520
521impl RenderableDiagnostic<'_> {
522 fn to_annotate(&self) -> AnnotateGroup<'_> {
524 let snippets = self.snippets_by_input.iter().flat_map(|snippets| {
525 let path = snippets.path;
526 snippets
527 .snippets
528 .iter()
529 .map(|snippet| snippet.to_annotate(path))
530 });
531 let mut title = self
532 .level
533 .clone()
534 .primary_title(self.message)
535 .is_fixable(self.is_fixable);
536 if let Some(id) = self.id {
537 title = title.id(id);
538 if let Some(url) = self.documentation_url {
539 title = title.id_url(url);
540 }
541 }
542 title.elements(snippets).lineno_offset(self.header_offset)
543 }
544}
545
546#[derive(Debug)]
548struct RenderableSnippets<'r> {
549 path: &'r str,
551 snippets: Vec<RenderableSnippet<'r>>,
553 has_primary: bool,
557}
558
559impl<'r> RenderableSnippets<'r> {
560 fn new<'a>(
577 context: usize,
578 path: &'r str,
579 resolved_snippets: &'a [Vec<&'r ResolvedAnnotation<'r>>],
580 ) -> RenderableSnippets<'r> {
581 assert!(!resolved_snippets.is_empty());
582
583 let mut has_primary = false;
584 let mut snippets = vec![];
585 for anns in resolved_snippets {
586 let snippet = RenderableSnippet::new(context, anns);
587 has_primary = has_primary || snippet.has_primary;
588 snippets.push(snippet);
589 }
590 snippets.sort_by(|s1, s2| s1.has_primary.cmp(&s2.has_primary).reverse());
591 RenderableSnippets {
592 path,
593 snippets,
594 has_primary,
595 }
596 }
597}
598
599#[derive(Debug)]
609struct RenderableSnippet<'r> {
610 snippet: Cow<'r, str>,
612 line_start: OneIndexed,
615 annotations: Vec<RenderableAnnotation<'r>>,
617 has_primary: bool,
620 cell_index: Option<usize>,
625}
626
627impl<'r> RenderableSnippet<'r> {
628 fn new<'a>(context: usize, anns: &'a [&'r ResolvedAnnotation<'r>]) -> RenderableSnippet<'r> {
652 assert!(
653 !anns.is_empty(),
654 "creating a renderable snippet requires a non-zero number of annotations",
655 );
656 let diagnostic_source = &anns[0].diagnostic_source;
657 let notebook_index = anns[0].notebook_index.as_ref();
658 let source = diagnostic_source.as_source_code();
659 let has_primary = anns.iter().any(|ann| ann.is_primary);
660
661 let content_start_index = anns.iter().map(|ann| ann.line_start).min().unwrap();
662 let line_start = context_before(&source, context, content_start_index, notebook_index);
663
664 let start = source.line_column(anns[0].range.start());
665 let cell_index = notebook_index
666 .map(|notebook_index| notebook_index.cell(start.line).unwrap_or_default().get());
667
668 let content_end_index = anns.iter().map(|ann| ann.line_end).max().unwrap();
669 let line_end = context_after(&source, context, content_end_index, notebook_index);
670
671 let snippet_start = source.line_start(line_start);
672 let snippet_end = source.line_end(line_end);
673 let snippet = diagnostic_source
674 .as_source_code()
675 .slice(TextRange::new(snippet_start, snippet_end));
676
677 const BOM: char = '\u{feff}';
682 let bom_len = BOM.text_len();
683 let (snippet, snippet_start) =
684 if snippet_start == TextSize::ZERO && snippet.starts_with(BOM) {
685 (
686 &snippet[bom_len.to_usize()..],
687 snippet_start + TextSize::new(bom_len.to_u32()),
688 )
689 } else {
690 (snippet, snippet_start)
691 };
692
693 let annotations = anns
694 .iter()
695 .map(|ann| RenderableAnnotation::new(snippet_start, ann))
696 .collect();
697
698 let EscapedSourceCode {
699 text: snippet,
700 annotations,
701 } = replace_unprintable(snippet, annotations).fix_up_empty_spans_after_line_terminator();
702
703 let line_start = notebook_index.map_or(line_start, |notebook_index| {
704 notebook_index
705 .cell_row(line_start)
706 .unwrap_or(OneIndexed::MIN)
707 });
708
709 RenderableSnippet {
710 snippet,
711 line_start,
712 annotations,
713 has_primary,
714 cell_index,
715 }
716 }
717
718 fn to_annotate<'a>(&'a self, path: &'a str) -> AnnotateSnippet<'a, AnnotateAnnotation<'a>> {
720 AnnotateSnippet::source(self.snippet.as_ref())
721 .path(path)
722 .line_start(self.line_start.get())
723 .fold(false)
724 .annotations(
725 self.annotations
726 .iter()
727 .map(RenderableAnnotation::to_annotate),
728 )
729 .cell_index(self.cell_index)
730 }
731}
732
733#[derive(Debug)]
735struct RenderableAnnotation<'r> {
736 range: TextRange,
740 message: Option<&'r str>,
742 is_primary: bool,
744 hide_snippet: bool,
746}
747
748impl<'r> RenderableAnnotation<'r> {
749 fn new(snippet_start: TextSize, ann: &'_ ResolvedAnnotation<'r>) -> RenderableAnnotation<'r> {
758 let range = ann.range.checked_sub(snippet_start).unwrap_or(ann.range);
763 RenderableAnnotation {
764 range,
765 message: ann.message,
766 is_primary: ann.is_primary,
767 hide_snippet: ann.hide_snippet,
768 }
769 }
770
771 fn to_annotate(&self) -> AnnotateAnnotation<'_> {
773 let kind = if self.is_primary {
774 AnnotationKind::Primary
775 } else {
776 AnnotationKind::Context
777 };
778 let mut ann = kind.span(self.range.into());
779 if let Some(message) = self.message {
780 ann = ann.label(message);
781 }
782 ann.hide_snippet(self.hide_snippet)
783 }
784}
785
786pub trait FileResolver {
802 fn path(&self, file: File) -> &str;
804
805 fn input(&self, file: File) -> Input;
807
808 fn notebook_index(&self, file: &UnifiedFile) -> Option<NotebookIndex>;
810
811 fn is_notebook(&self, file: &UnifiedFile) -> bool;
813
814 fn current_directory(&self) -> &Path;
816}
817
818impl<T> FileResolver for T
819where
820 T: Db,
821{
822 fn path(&self, file: File) -> &str {
823 file.path(self).as_str()
824 }
825
826 fn input(&self, file: File) -> Input {
827 Input {
828 text: source_text(self, file),
829 line_index: line_index(self, file),
830 }
831 }
832
833 fn notebook_index(&self, file: &UnifiedFile) -> Option<NotebookIndex> {
834 match file {
835 UnifiedFile::Ty(file) => self
836 .input(*file)
837 .text
838 .as_notebook()
839 .map(Notebook::index)
840 .cloned(),
841 UnifiedFile::Ruff(_) => unimplemented!("Expected an interned ty file"),
842 }
843 }
844
845 fn is_notebook(&self, file: &UnifiedFile) -> bool {
846 match file {
847 UnifiedFile::Ty(file) => self.input(*file).text.as_notebook().is_some(),
848 UnifiedFile::Ruff(_) => unimplemented!("Expected an interned ty file"),
849 }
850 }
851
852 fn current_directory(&self) -> &Path {
853 self.system().current_directory().as_std_path()
854 }
855}
856
857impl FileResolver for &dyn Db {
858 fn path(&self, file: File) -> &str {
859 file.path(*self).as_str()
860 }
861
862 fn input(&self, file: File) -> Input {
863 Input {
864 text: source_text(*self, file),
865 line_index: line_index(*self, file),
866 }
867 }
868
869 fn notebook_index(&self, file: &UnifiedFile) -> Option<NotebookIndex> {
870 match file {
871 UnifiedFile::Ty(file) => self
872 .input(*file)
873 .text
874 .as_notebook()
875 .map(Notebook::index)
876 .cloned(),
877 UnifiedFile::Ruff(_) => unimplemented!("Expected an interned ty file"),
878 }
879 }
880
881 fn is_notebook(&self, file: &UnifiedFile) -> bool {
882 match file {
883 UnifiedFile::Ty(file) => self.input(*file).text.as_notebook().is_some(),
884 UnifiedFile::Ruff(_) => unimplemented!("Expected an interned ty file"),
885 }
886 }
887
888 fn current_directory(&self) -> &Path {
889 self.system().current_directory().as_std_path()
890 }
891}
892
893#[derive(Clone, Debug)]
899pub struct Input {
900 pub(crate) text: SourceText,
901 pub(crate) line_index: LineIndex,
902}
903
904fn context_before(
913 source: &SourceCode<'_, '_>,
914 len: usize,
915 start: OneIndexed,
916 notebook_index: Option<&NotebookIndex>,
917) -> OneIndexed {
918 let mut line = start.saturating_sub(len);
919 while line < start {
921 if !source.line_text(line).trim().is_empty() {
922 break;
923 }
924 line = line.saturating_add(1);
925 }
926
927 if let Some(index) = notebook_index {
928 let content_start_cell = index.cell(start).unwrap_or(OneIndexed::MIN);
929 while line < start {
930 if index.cell(line).unwrap_or(OneIndexed::MIN) == content_start_cell {
931 break;
932 }
933 line = line.saturating_add(1);
934 }
935 }
936
937 line
938}
939
940fn context_after(
950 source: &SourceCode<'_, '_>,
951 len: usize,
952 start: OneIndexed,
953 notebook_index: Option<&NotebookIndex>,
954) -> OneIndexed {
955 let max_lines = OneIndexed::from_zero_indexed(source.line_count());
956 let mut line = start.saturating_add(len).min(max_lines);
957 while line > start {
959 if !source.line_text(line).trim().is_empty() {
960 break;
961 }
962 line = line.saturating_sub(1);
963 }
964
965 if let Some(index) = notebook_index {
966 let content_end_cell = index.cell(start).unwrap_or(OneIndexed::MIN);
967 while line > start {
968 if index.cell(line).unwrap_or(OneIndexed::MIN) == content_end_cell {
969 break;
970 }
971 line = line.saturating_sub(1);
972 }
973 }
974
975 line
976}
977
978fn replace_unprintable<'r>(
990 source: &'r str,
991 mut annotations: Vec<RenderableAnnotation<'r>>,
992) -> EscapedSourceCode<'r> {
993 let mut update_ranges = |index: usize, len: u32| {
1000 for ann in &mut annotations {
1001 if index < usize::from(ann.range.start()) {
1002 ann.range += TextSize::new(len - 1);
1003 } else if index < usize::from(ann.range.end()) {
1004 ann.range = ann.range.add_end(TextSize::new(len - 1));
1005 }
1006 }
1007 };
1008
1009 let unprintable_replacement = |c: char| -> Option<char> {
1012 match c {
1013 '\x07' => Some('␇'),
1014 '\x08' => Some('␈'),
1015 '\x1b' => Some('␛'),
1016 '\x7f' => Some('␡'),
1017 _ => None,
1018 }
1019 };
1020
1021 let mut last_end = 0;
1022 let mut result = String::new();
1023 for (index, c) in source.char_indices() {
1024 if c == '\r' && !source[index + 1..].starts_with("\n") {
1026 result.push_str(&source[last_end..index]);
1027 result.push('\n');
1028 last_end = index + 1;
1029 } else if let Some(printable) = unprintable_replacement(c) {
1030 result.push_str(&source[last_end..index]);
1031
1032 let len = printable.text_len().to_u32();
1033 update_ranges(result.text_len().to_usize(), len);
1034
1035 result.push(printable);
1036 last_end = index + 1;
1037 }
1038 }
1039
1040 if result.is_empty() {
1042 EscapedSourceCode {
1043 annotations,
1044 text: Cow::Borrowed(source),
1045 }
1046 } else {
1047 result.push_str(&source[last_end..]);
1048 EscapedSourceCode {
1049 annotations,
1050 text: Cow::Owned(result),
1051 }
1052 }
1053}
1054
1055struct EscapedSourceCode<'r> {
1056 text: Cow<'r, str>,
1057 annotations: Vec<RenderableAnnotation<'r>>,
1058}
1059
1060impl<'r> EscapedSourceCode<'r> {
1061 fn fix_up_empty_spans_after_line_terminator(mut self) -> EscapedSourceCode<'r> {
1078 for ann in &mut self.annotations {
1079 let range = ann.range;
1080 if !range.is_empty()
1081 || range.start() == TextSize::from(0)
1082 || range.start() >= self.text.text_len()
1083 {
1084 continue;
1085 }
1086 if !matches!(
1087 self.text.as_bytes()[range.start().to_usize() - 1],
1088 b'\n' | b'\r'
1089 ) {
1090 continue;
1091 }
1092 let start = range.start();
1093 let end =
1094 TextSize::try_from(self.text.ceil_char_boundary(start.to_usize() + 1)).unwrap();
1095 ann.range = TextRange::new(start, end);
1096 }
1097
1098 self
1099 }
1100}
1101
1102pub struct DummyFileResolver;
1104
1105impl FileResolver for DummyFileResolver {
1106 fn path(&self, _file: File) -> &str {
1107 unimplemented!()
1108 }
1109
1110 fn input(&self, _file: File) -> Input {
1111 unimplemented!()
1112 }
1113
1114 fn notebook_index(&self, _file: &UnifiedFile) -> Option<NotebookIndex> {
1115 None
1116 }
1117
1118 fn is_notebook(&self, _file: &UnifiedFile) -> bool {
1119 false
1120 }
1121
1122 fn current_directory(&self) -> &Path {
1123 Path::new(".")
1124 }
1125}
1126
1127#[cfg(test)]
1128mod tests {
1129
1130 use ruff_diagnostics::{Applicability, Edit, Fix};
1131
1132 use crate::diagnostic::{
1133 Annotation, DiagnosticId, IntoDiagnosticMessage, SecondaryCode, Severity, Span,
1134 SubDiagnosticSeverity,
1135 };
1136 use crate::files::system_path_to_file;
1137 use crate::system::{DbWithWritableSystem, SystemPath};
1138 use crate::tests::TestDb;
1139
1140 use super::*;
1141
1142 static ANIMALS: &str = "\
1143aardvark
1144beetle
1145canary
1146dog
1147elephant
1148finch
1149gorilla
1150hippopotamus
1151inchworm
1152jackrabbit
1153kangaroo
1154";
1155
1156 static SPACEY_ANIMALS: &str = "\
1159aardvark
1160
1161beetle
1162
1163canary
1164
1165dog
1166elephant
1167finch
1168
1169gorilla
1170hippopotamus
1171inchworm
1172jackrabbit
1173
1174kangaroo
1175";
1176
1177 static FRUITS: &str = "\
1178apple
1179banana
1180cantaloupe
1181lime
1182orange
1183pear
1184raspberry
1185strawberry
1186tomato
1187watermelon
1188";
1189
1190 static NON_ASCII: &str = "\
1191☃☃☃☃☃☃☃☃☃☃☃☃
1192💩💩💩💩💩💩💩💩💩💩💩💩
1193ΔΔΔΔΔΔΔΔΔΔΔΔ
1194ββββββββββββ
1195ΣΣΣΣΣΣΣΣΣΣΣΣ
1196ξξξξξξξξξξξξ
1197ππππππππππππ
1198θθθθθθθθθθθθ
1199ΦΦΦΦΦΦΦΦΦΦΦΦ
1200λλλλλλλλλλλλ
1201";
1202
1203 #[test]
1204 fn basic() {
1205 let mut env = TestEnvironment::new();
1206 env.add("animals", ANIMALS);
1207
1208 let diag = env.err().primary("animals", "5", "5", "").build();
1209 insta::assert_snapshot!(
1210 env.render(&diag),
1211 @"
1212 error[test-diagnostic]: main diagnostic message
1213 --> animals:5:1
1214 |
1215 3 | canary
1216 4 | dog
1217 5 | elephant
1218 | ^^^^^^^^
1219 6 | finch
1220 7 | gorilla
1221 |
1222 ",
1223 );
1224
1225 let diag = env
1226 .builder(
1227 "test-diagnostic",
1228 Severity::Warning,
1229 "main diagnostic message",
1230 )
1231 .primary("animals", "5", "5", "")
1232 .build();
1233 insta::assert_snapshot!(
1234 env.render(&diag),
1235 @"
1236 warning[test-diagnostic]: main diagnostic message
1237 --> animals:5:1
1238 |
1239 3 | canary
1240 4 | dog
1241 5 | elephant
1242 | ^^^^^^^^
1243 6 | finch
1244 7 | gorilla
1245 |
1246 ",
1247 );
1248
1249 let diag = env
1250 .builder("test-diagnostic", Severity::Info, "main diagnostic message")
1251 .primary("animals", "5", "5", "")
1252 .build();
1253 insta::assert_snapshot!(
1254 env.render(&diag),
1255 @"
1256 info[test-diagnostic]: main diagnostic message
1257 --> animals:5:1
1258 |
1259 3 | canary
1260 4 | dog
1261 5 | elephant
1262 | ^^^^^^^^
1263 6 | finch
1264 7 | gorilla
1265 |
1266 ",
1267 );
1268 }
1269
1270 #[test]
1271 fn no_range() {
1272 let mut env = TestEnvironment::new();
1273 env.add("animals", ANIMALS);
1274
1275 let mut builder = env.err();
1276 builder
1277 .diag
1278 .annotate(Annotation::primary(builder.env.path("animals")));
1279 let diag = builder.build();
1280 insta::assert_snapshot!(
1281 env.render(&diag),
1282 @"
1283 error[test-diagnostic]: main diagnostic message
1284 --> animals:1:1
1285 |
1286 1 | aardvark
1287 | ^
1288 2 | beetle
1289 3 | canary
1290 |
1291 ",
1292 );
1293
1294 let mut builder = env.err();
1295 builder.diag.annotate(
1296 Annotation::primary(builder.env.path("animals")).message("primary annotation message"),
1297 );
1298 let diag = builder.build();
1299 insta::assert_snapshot!(
1300 env.render(&diag),
1301 @"
1302 error[test-diagnostic]: main diagnostic message
1303 --> animals:1:1
1304 |
1305 1 | aardvark
1306 | ^ primary annotation message
1307 2 | beetle
1308 3 | canary
1309 |
1310 ",
1311 );
1312 }
1313
1314 #[test]
1315 fn non_ascii() {
1316 let mut env = TestEnvironment::new();
1317 env.add("non-ascii", NON_ASCII);
1318
1319 let diag = env.err().primary("non-ascii", "5", "5", "").build();
1320 insta::assert_snapshot!(
1321 env.render(&diag),
1322 @"
1323 error[test-diagnostic]: main diagnostic message
1324 --> non-ascii:5:1
1325 |
1326 3 | ΔΔΔΔΔΔΔΔΔΔΔΔ
1327 4 | ββββββββββββ
1328 5 | ΣΣΣΣΣΣΣΣΣΣΣΣ
1329 | ^^^^^^^^^^^^
1330 6 | ξξξξξξξξξξξξ
1331 7 | ππππππππππππ
1332 |
1333 ",
1334 );
1335
1336 let diag = env.err().primary("non-ascii", "2:4", "2:8", "").build();
1339 insta::assert_snapshot!(
1340 env.render(&diag),
1341 @"
1342 error[test-diagnostic]: main diagnostic message
1343 --> non-ascii:2:2
1344 |
1345 1 | ☃☃☃☃☃☃☃☃☃☃☃☃
1346 2 | 💩💩💩💩💩💩💩💩💩💩💩💩
1347 | ^^
1348 3 | ΔΔΔΔΔΔΔΔΔΔΔΔ
1349 4 | ββββββββββββ
1350 |
1351 ",
1352 );
1353 }
1354
1355 #[test]
1356 fn config_context() {
1357 let mut env = TestEnvironment::new();
1358 env.add("animals", ANIMALS);
1359
1360 let diag = env.err().primary("animals", "5", "5", "").build();
1362 env.context(1);
1363 insta::assert_snapshot!(
1364 env.render(&diag),
1365 @"
1366 error[test-diagnostic]: main diagnostic message
1367 --> animals:5:1
1368 |
1369 4 | dog
1370 5 | elephant
1371 | ^^^^^^^^
1372 6 | finch
1373 |
1374 ",
1375 );
1376
1377 let diag = env.err().primary("animals", "5", "5", "").build();
1379 env.context(0);
1380 insta::assert_snapshot!(
1381 env.render(&diag),
1382 @"
1383 error[test-diagnostic]: main diagnostic message
1384 --> animals:5:1
1385 |
1386 5 | elephant
1387 | ^^^^^^^^
1388 ",
1389 );
1390
1391 let diag = env.err().primary("animals", "1", "1", "").build();
1393 env.context(2);
1394 insta::assert_snapshot!(
1395 env.render(&diag),
1396 @"
1397 error[test-diagnostic]: main diagnostic message
1398 --> animals:1:1
1399 |
1400 1 | aardvark
1401 | ^^^^^^^^
1402 2 | beetle
1403 3 | canary
1404 |
1405 ",
1406 );
1407
1408 let diag = env.err().primary("animals", "11", "11", "").build();
1410 env.context(2);
1411 insta::assert_snapshot!(
1412 env.render(&diag),
1413 @"
1414 error[test-diagnostic]: main diagnostic message
1415 --> animals:11:1
1416 |
1417 9 | inchworm
1418 10 | jackrabbit
1419 11 | kangaroo
1420 | ^^^^^^^^
1421 ",
1422 );
1423
1424 let diag = env.err().primary("animals", "5", "5", "").build();
1426 env.context(200);
1427 insta::assert_snapshot!(
1428 env.render(&diag),
1429 @"
1430 error[test-diagnostic]: main diagnostic message
1431 --> animals:5:1
1432 |
1433 1 | aardvark
1434 2 | beetle
1435 3 | canary
1436 4 | dog
1437 5 | elephant
1438 | ^^^^^^^^
1439 6 | finch
1440 7 | gorilla
1441 8 | hippopotamus
1442 9 | inchworm
1443 10 | jackrabbit
1444 11 | kangaroo
1445 |
1446 ",
1447 );
1448 }
1449
1450 #[test]
1451 fn multiple_annotations_non_overlapping() {
1452 let mut env = TestEnvironment::new();
1453 env.add("animals", ANIMALS);
1454
1455 let diag = env
1456 .err()
1457 .primary("animals", "1", "1", "")
1458 .primary("animals", "11", "11", "")
1459 .build();
1460 insta::assert_snapshot!(
1461 env.render(&diag),
1462 @"
1463 error[test-diagnostic]: main diagnostic message
1464 --> animals:1:1
1465 |
1466 1 | aardvark
1467 | ^^^^^^^^
1468 2 | beetle
1469 3 | canary
1470 |
1471 ::: animals:11:1
1472 |
1473 9 | inchworm
1474 10 | jackrabbit
1475 11 | kangaroo
1476 | ^^^^^^^^
1477 ",
1478 );
1479 }
1480
1481 #[test]
1482 fn multiple_annotations_adjacent_context() {
1483 let mut env = TestEnvironment::new();
1484 env.add("animals", ANIMALS);
1485
1486 env.context(1);
1491
1492 let diag = env
1493 .err()
1494 .primary("animals", "1", "1", "")
1495 .primary("animals", "3", "3", "")
1502 .build();
1503 insta::assert_snapshot!(
1504 env.render(&diag),
1505 @"
1506 error[test-diagnostic]: main diagnostic message
1507 --> animals:1:1
1508 |
1509 1 | aardvark
1510 | ^^^^^^^^
1511 2 | beetle
1512 3 | canary
1513 | ^^^^^^
1514 4 | dog
1515 |
1516 ",
1517 );
1518
1519 let diag = env
1524 .err()
1525 .primary("animals", "1", "1", "")
1526 .primary("animals", "4", "4", "")
1527 .build();
1528 insta::assert_snapshot!(
1529 env.render(&diag),
1530 @"
1531 error[test-diagnostic]: main diagnostic message
1532 --> animals:1:1
1533 |
1534 1 | aardvark
1535 | ^^^^^^^^
1536 2 | beetle
1537 3 | canary
1538 4 | dog
1539 | ^^^
1540 5 | elephant
1541 |
1542 ",
1543 );
1544
1545 let diag = env
1552 .err()
1553 .primary("animals", "1", "1", "")
1554 .primary("animals", "5", "5", "")
1555 .build();
1556 insta::assert_snapshot!(
1557 env.render(&diag),
1558 @"
1559 error[test-diagnostic]: main diagnostic message
1560 --> animals:1:1
1561 |
1562 1 | aardvark
1563 | ^^^^^^^^
1564 2 | beetle
1565 |
1566 ::: animals:5:1
1567 |
1568 4 | dog
1569 5 | elephant
1570 | ^^^^^^^^
1571 6 | finch
1572 |
1573 ",
1574 );
1575
1576 env.context(3);
1579 let diag = env
1580 .err()
1581 .primary("animals", "1", "1", "")
1582 .primary("animals", "5", "5", "")
1583 .build();
1584 insta::assert_snapshot!(
1585 env.render(&diag),
1586 @"
1587 error[test-diagnostic]: main diagnostic message
1588 --> animals:1:1
1589 |
1590 1 | aardvark
1591 | ^^^^^^^^
1592 2 | beetle
1593 3 | canary
1594 4 | dog
1595 5 | elephant
1596 | ^^^^^^^^
1597 6 | finch
1598 7 | gorilla
1599 8 | hippopotamus
1600 |
1601 ",
1602 );
1603
1604 let diag = env
1605 .err()
1606 .primary("animals", "1", "1", "")
1607 .primary("animals", "8", "8", "")
1608 .build();
1609 insta::assert_snapshot!(
1610 env.render(&diag),
1611 @"
1612 error[test-diagnostic]: main diagnostic message
1613 --> animals:1:1
1614 |
1615 1 | aardvark
1616 | ^^^^^^^^
1617 2 | beetle
1618 3 | canary
1619 4 | dog
1620 5 | elephant
1621 6 | finch
1622 7 | gorilla
1623 8 | hippopotamus
1624 | ^^^^^^^^^^^^
1625 9 | inchworm
1626 10 | jackrabbit
1627 11 | kangaroo
1628 |
1629 ",
1630 );
1631
1632 let diag = env
1633 .err()
1634 .primary("animals", "1", "1", "")
1635 .primary("animals", "9", "9", "")
1636 .build();
1637 insta::assert_snapshot!(
1641 env.render(&diag),
1642 @"
1643 error[test-diagnostic]: main diagnostic message
1644 --> animals:1:1
1645 |
1646 1 | aardvark
1647 | ^^^^^^^^
1648 2 | beetle
1649 3 | canary
1650 4 | dog
1651 |
1652 ::: animals:9:1
1653 |
1654 6 | finch
1655 7 | gorilla
1656 8 | hippopotamus
1657 9 | inchworm
1658 | ^^^^^^^^
1659 10 | jackrabbit
1660 11 | kangaroo
1661 |
1662 ",
1663 );
1664 }
1665
1666 #[test]
1667 fn trimmed_context() {
1668 let mut env = TestEnvironment::new();
1669 env.add("spacey-animals", SPACEY_ANIMALS);
1670
1671 env.context(2);
1677 let diag = env.err().primary("spacey-animals", "8", "8", "").build();
1678 insta::assert_snapshot!(
1679 env.render(&diag),
1680 @"
1681 error[test-diagnostic]: main diagnostic message
1682 --> spacey-animals:8:1
1683 |
1684 7 | dog
1685 8 | elephant
1686 | ^^^^^^^^
1687 9 | finch
1688 |
1689 ",
1690 );
1691
1692 let diag = env.err().primary("spacey-animals", "12", "12", "").build();
1695 insta::assert_snapshot!(
1696 env.render(&diag),
1697 @"
1698 error[test-diagnostic]: main diagnostic message
1699 --> spacey-animals:12:1
1700 |
1701 11 | gorilla
1702 12 | hippopotamus
1703 | ^^^^^^^^^^^^
1704 13 | inchworm
1705 14 | jackrabbit
1706 |
1707 ",
1708 );
1709
1710 let diag = env.err().primary("spacey-animals", "13", "13", "").build();
1713 insta::assert_snapshot!(
1714 env.render(&diag),
1715 @"
1716 error[test-diagnostic]: main diagnostic message
1717 --> spacey-animals:13:1
1718 |
1719 11 | gorilla
1720 12 | hippopotamus
1721 13 | inchworm
1722 | ^^^^^^^^
1723 14 | jackrabbit
1724 |
1725 ",
1726 );
1727 }
1728
1729 #[test]
1730 fn multiple_annotations_trimmed_context() {
1731 let mut env = TestEnvironment::new();
1732 env.add("spacey-animals", SPACEY_ANIMALS);
1733
1734 env.context(1);
1735 let diag = env
1736 .err()
1737 .primary("spacey-animals", "3", "3", "")
1738 .primary("spacey-animals", "5", "5", "")
1739 .build();
1740 insta::assert_snapshot!(
1754 env.render(&diag),
1755 @"
1756 error[test-diagnostic]: main diagnostic message
1757 --> spacey-animals:3:1
1758 |
1759 3 | beetle
1760 | ^^^^^^
1761 |
1762 ::: spacey-animals:5:1
1763 |
1764 5 | canary
1765 | ^^^^^^
1766 ",
1767 );
1768 }
1769
1770 #[test]
1771 fn multiple_files_basic() {
1772 let mut env = TestEnvironment::new();
1773 env.add("animals", ANIMALS);
1774 env.add("fruits", FRUITS);
1775
1776 let diag = env
1777 .err()
1778 .primary("animals", "3", "3", "")
1779 .primary("fruits", "3", "3", "")
1780 .build();
1781 insta::assert_snapshot!(
1782 env.render(&diag),
1783 @"
1784 error[test-diagnostic]: main diagnostic message
1785 --> animals:3:1
1786 |
1787 1 | aardvark
1788 2 | beetle
1789 3 | canary
1790 | ^^^^^^
1791 4 | dog
1792 5 | elephant
1793 |
1794 ::: fruits:3:1
1795 |
1796 1 | apple
1797 2 | banana
1798 3 | cantaloupe
1799 | ^^^^^^^^^^
1800 4 | lime
1801 5 | orange
1802 |
1803 ",
1804 );
1805 }
1806
1807 #[test]
1808 fn sub_diag_note_only_message() {
1809 let mut env = TestEnvironment::new();
1810 env.add("animals", ANIMALS);
1811 env.add("fruits", FRUITS);
1812
1813 let mut diag = env.err().primary("animals", "3", "3", "").build();
1814 diag.sub(
1815 env.sub_builder(SubDiagnosticSeverity::Info, "this is a helpful note")
1816 .build(),
1817 );
1818 insta::assert_snapshot!(
1819 env.render(&diag),
1820 @"
1821 error[test-diagnostic]: main diagnostic message
1822 --> animals:3:1
1823 |
1824 1 | aardvark
1825 2 | beetle
1826 3 | canary
1827 | ^^^^^^
1828 4 | dog
1829 5 | elephant
1830 |
1831 info: this is a helpful note
1832 ",
1833 );
1834 }
1835
1836 #[test]
1837 fn sub_diag_many_notes() {
1838 let mut env = TestEnvironment::new();
1839 env.add("animals", ANIMALS);
1840 env.add("fruits", FRUITS);
1841
1842 let mut diag = env.err().primary("animals", "3", "3", "").build();
1843 diag.sub(
1844 env.sub_builder(SubDiagnosticSeverity::Info, "this is a helpful note")
1845 .build(),
1846 );
1847 diag.sub(
1848 env.sub_builder(SubDiagnosticSeverity::Info, "another helpful note")
1849 .build(),
1850 );
1851 diag.sub(
1852 env.sub_builder(SubDiagnosticSeverity::Info, "and another helpful note")
1853 .build(),
1854 );
1855 insta::assert_snapshot!(
1856 env.render(&diag),
1857 @"
1858 error[test-diagnostic]: main diagnostic message
1859 --> animals:3:1
1860 |
1861 1 | aardvark
1862 2 | beetle
1863 3 | canary
1864 | ^^^^^^
1865 4 | dog
1866 5 | elephant
1867 |
1868 info: this is a helpful note
1869 info: another helpful note
1870 info: and another helpful note
1871 ",
1872 );
1873 }
1874
1875 #[test]
1876 fn sub_diag_warning_with_annotation() {
1877 let mut env = TestEnvironment::new();
1878 env.add("animals", ANIMALS);
1879 env.add("fruits", FRUITS);
1880
1881 let mut diag = env.err().primary("animals", "3", "3", "").build();
1882 diag.sub(env.sub_warn().primary("fruits", "3", "3", "").build());
1883 insta::assert_snapshot!(
1884 env.render(&diag),
1885 @"
1886 error[test-diagnostic]: main diagnostic message
1887 --> animals:3:1
1888 |
1889 1 | aardvark
1890 2 | beetle
1891 3 | canary
1892 | ^^^^^^
1893 4 | dog
1894 5 | elephant
1895 |
1896 warning: sub-diagnostic message
1897 --> fruits:3:1
1898 |
1899 1 | apple
1900 2 | banana
1901 3 | cantaloupe
1902 | ^^^^^^^^^^
1903 4 | lime
1904 5 | orange
1905 |
1906 ",
1907 );
1908 }
1909
1910 #[test]
1911 fn sub_diag_many_warning_with_annotation_order() {
1912 let mut env = TestEnvironment::new();
1913 env.add("animals", ANIMALS);
1914 env.add("fruits", FRUITS);
1915
1916 let mut diag = env.err().primary("animals", "3", "3", "").build();
1917 diag.sub(env.sub_warn().primary("fruits", "3", "3", "").build());
1918 diag.sub(env.sub_warn().primary("animals", "11", "11", "").build());
1919 insta::assert_snapshot!(
1920 env.render(&diag),
1921 @"
1922 error[test-diagnostic]: main diagnostic message
1923 --> animals:3:1
1924 |
1925 1 | aardvark
1926 2 | beetle
1927 3 | canary
1928 | ^^^^^^
1929 4 | dog
1930 5 | elephant
1931 |
1932 warning: sub-diagnostic message
1933 --> fruits:3:1
1934 |
1935 1 | apple
1936 2 | banana
1937 3 | cantaloupe
1938 | ^^^^^^^^^^
1939 4 | lime
1940 5 | orange
1941 |
1942 warning: sub-diagnostic message
1943 --> animals:11:1
1944 |
1945 9 | inchworm
1946 10 | jackrabbit
1947 11 | kangaroo
1948 | ^^^^^^^^
1949 ",
1950 );
1951
1952 let mut diag = env.err().primary("animals", "3", "3", "").build();
1955 diag.sub(env.sub_warn().primary("animals", "11", "11", "").build());
1956 diag.sub(env.sub_warn().primary("fruits", "3", "3", "").build());
1957 insta::assert_snapshot!(
1958 env.render(&diag),
1959 @"
1960 error[test-diagnostic]: main diagnostic message
1961 --> animals:3:1
1962 |
1963 1 | aardvark
1964 2 | beetle
1965 3 | canary
1966 | ^^^^^^
1967 4 | dog
1968 5 | elephant
1969 |
1970 warning: sub-diagnostic message
1971 --> animals:11:1
1972 |
1973 9 | inchworm
1974 10 | jackrabbit
1975 11 | kangaroo
1976 | ^^^^^^^^
1977 warning: sub-diagnostic message
1978 --> fruits:3:1
1979 |
1980 1 | apple
1981 2 | banana
1982 3 | cantaloupe
1983 | ^^^^^^^^^^
1984 4 | lime
1985 5 | orange
1986 |
1987 ",
1988 );
1989 }
1990
1991 #[test]
1992 fn sub_diag_repeats_snippet() {
1993 let mut env = TestEnvironment::new();
1994 env.add("animals", ANIMALS);
1995
1996 let mut diag = env.err().primary("animals", "3", "3", "").build();
1997 diag.sub(env.sub_warn().secondary("animals", "3", "3", "").build());
2004 insta::assert_snapshot!(
2005 env.render(&diag),
2006 @"
2007 error[test-diagnostic]: main diagnostic message
2008 --> animals:3:1
2009 |
2010 1 | aardvark
2011 2 | beetle
2012 3 | canary
2013 | ^^^^^^
2014 4 | dog
2015 5 | elephant
2016 |
2017 warning: sub-diagnostic message
2018 --> animals:3:1
2019 |
2020 1 | aardvark
2021 2 | beetle
2022 3 | canary
2023 | ------
2024 4 | dog
2025 5 | elephant
2026 |
2027 ",
2028 );
2029 }
2030
2031 #[test]
2032 fn annotation_multi_line() {
2033 let mut env = TestEnvironment::new();
2034 env.add("animals", ANIMALS);
2035
2036 let diag = env.err().primary("animals", "5", "6", "").build();
2040 insta::assert_snapshot!(
2041 env.render(&diag),
2042 @"
2043 error[test-diagnostic]: main diagnostic message
2044 --> animals:5:1
2045 |
2046 3 | canary
2047 4 | dog
2048 5 | / elephant
2049 6 | | finch
2050 | |_____^
2051 7 | gorilla
2052 8 | hippopotamus
2053 |
2054 ",
2055 );
2056
2057 let diag = env.err().primary("animals", "5", "7:0", "").build();
2063 insta::assert_snapshot!(
2064 env.render(&diag),
2065 @"
2066 error[test-diagnostic]: main diagnostic message
2067 --> animals:5:1
2068 |
2069 3 | canary
2070 4 | dog
2071 5 | / elephant
2072 6 | | finch
2073 | |_____^
2074 7 | gorilla
2075 8 | hippopotamus
2076 |
2077 ",
2078 );
2079
2080 let diag = env.err().primary("animals", "5", "7:1", "").build();
2083 insta::assert_snapshot!(
2084 env.render(&diag),
2085 @"
2086 error[test-diagnostic]: main diagnostic message
2087 --> animals:5:1
2088 |
2089 3 | canary
2090 4 | dog
2091 5 | / elephant
2092 6 | | finch
2093 7 | | gorilla
2094 | |_^
2095 8 | hippopotamus
2096 9 | inchworm
2097 |
2098 ",
2099 );
2100
2101 let diag = env.err().primary("animals", "5:3", "8:8", "").build();
2103 insta::assert_snapshot!(
2104 env.render(&diag),
2105 @"
2106 error[test-diagnostic]: main diagnostic message
2107 --> animals:5:4
2108 |
2109 3 | canary
2110 4 | dog
2111 5 | elephant
2112 | ____^
2113 6 | | finch
2114 7 | | gorilla
2115 8 | | hippopotamus
2116 | |________^
2117 9 | inchworm
2118 10 | jackrabbit
2119 |
2120 ",
2121 );
2122
2123 let diag = env.err().secondary("animals", "5:3", "8:8", "").build();
2125 insta::assert_snapshot!(
2126 env.render(&diag),
2127 @"
2128 error[test-diagnostic]: main diagnostic message
2129 --> animals:5:4
2130 |
2131 3 | canary
2132 4 | dog
2133 5 | elephant
2134 | ____-
2135 6 | | finch
2136 7 | | gorilla
2137 8 | | hippopotamus
2138 | |________-
2139 9 | inchworm
2140 10 | jackrabbit
2141 |
2142 ",
2143 );
2144 }
2145
2146 #[test]
2147 fn annotation_overlapping_multi_line() {
2148 let mut env = TestEnvironment::new();
2149 env.add("animals", ANIMALS);
2150
2151 let diag = env
2153 .err()
2154 .primary("animals", "5", "6", "")
2155 .primary("animals", "4", "7", "")
2156 .build();
2157 insta::assert_snapshot!(
2158 env.render(&diag),
2159 @"
2160 error[test-diagnostic]: main diagnostic message
2161 --> animals:4:1
2162 |
2163 2 | beetle
2164 3 | canary
2165 4 | / dog
2166 5 | |/ elephant
2167 6 | || finch
2168 | ||_____^
2169 7 | | gorilla
2170 | |________^
2171 8 | hippopotamus
2172 9 | inchworm
2173 |
2174 ",
2175 );
2176
2177 let diag = env
2180 .err()
2181 .primary("animals", "4", "7", "")
2182 .primary("animals", "5", "6", "")
2183 .build();
2184 insta::assert_snapshot!(
2185 env.render(&diag),
2186 @"
2187 error[test-diagnostic]: main diagnostic message
2188 --> animals:4:1
2189 |
2190 2 | beetle
2191 3 | canary
2192 4 | / dog
2193 5 | |/ elephant
2194 6 | || finch
2195 | ||_____^
2196 7 | | gorilla
2197 | |________^
2198 8 | hippopotamus
2199 9 | inchworm
2200 |
2201 ",
2202 );
2203
2204 let diag = env
2209 .err()
2210 .primary("animals", "5", "7", "")
2211 .primary("animals", "6", "7", "")
2212 .build();
2213 insta::assert_snapshot!(
2214 env.render(&diag),
2215 @"
2216 error[test-diagnostic]: main diagnostic message
2217 --> animals:5:1
2218 |
2219 3 | canary
2220 4 | dog
2221 5 | / elephant
2222 6 | |/ finch
2223 7 | || gorilla
2224 | ||_______^
2225 | |_______|
2226 |
2227 8 | hippopotamus
2228 9 | inchworm
2229 |
2230 ",
2231 );
2232
2233 let diag = env
2238 .err()
2239 .primary("animals", "5", "6", "")
2240 .primary("animals", "5", "7", "")
2241 .build();
2242 insta::assert_snapshot!(
2247 env.render(&diag),
2248 @"
2249 error[test-diagnostic]: main diagnostic message
2250 --> animals:5:1
2251 |
2252 3 | canary
2253 4 | dog
2254 5 | // elephant
2255 6 | || finch
2256 | ||_____^
2257 7 | | gorilla
2258 | |________^
2259 8 | hippopotamus
2260 9 | inchworm
2261 |
2262 ",
2263 );
2264
2265 let diag = env
2268 .err()
2269 .primary("animals", "5", "6", "")
2270 .primary("animals", "6", "7", "")
2271 .build();
2272 insta::assert_snapshot!(
2273 env.render(&diag),
2274 @"
2275 error[test-diagnostic]: main diagnostic message
2276 --> animals:5:1
2277 |
2278 3 | canary
2279 4 | dog
2280 5 | / elephant
2281 6 | | finch
2282 | |__^___^
2283 | _|
2284 | |
2285 7 | | gorilla
2286 | |_______^
2287 8 | hippopotamus
2288 9 | inchworm
2289 |
2290 ",
2291 );
2292 }
2293
2294 #[test]
2295 fn annotation_message() {
2296 let mut env = TestEnvironment::new();
2297 env.add("animals", ANIMALS);
2298
2299 let diag = env
2300 .err()
2301 .primary("animals", "5:2", "5:6", "giant land mammal")
2302 .build();
2303 insta::assert_snapshot!(
2304 env.render(&diag),
2305 @"
2306 error[test-diagnostic]: main diagnostic message
2307 --> animals:5:3
2308 |
2309 3 | canary
2310 4 | dog
2311 5 | elephant
2312 | ^^^^ giant land mammal
2313 6 | finch
2314 7 | gorilla
2315 |
2316 ",
2317 );
2318
2319 let diag = env
2321 .err()
2322 .primary("animals", "5:2", "5:6", "giant land mammal")
2323 .secondary("animals", "5:2", "5:6", "but afraid of mice")
2324 .build();
2325 insta::assert_snapshot!(
2326 env.render(&diag),
2327 @"
2328 error[test-diagnostic]: main diagnostic message
2329 --> animals:5:3
2330 |
2331 3 | canary
2332 4 | dog
2333 5 | elephant
2334 | ^^^^
2335 | |
2336 | giant land mammal
2337 | but afraid of mice
2338 6 | finch
2339 7 | gorilla
2340 |
2341 ",
2342 );
2343 }
2344
2345 #[test]
2346 fn annotation_one_file_primary_always_comes_first() {
2347 let mut env = TestEnvironment::new();
2348 env.add("animals", ANIMALS);
2349
2350 let diag = env
2354 .err()
2355 .secondary("animals", "1", "1", "secondary")
2356 .primary("animals", "8", "8", "primary")
2357 .build();
2358 insta::assert_snapshot!(
2359 env.render(&diag),
2360 @"
2361 error[test-diagnostic]: main diagnostic message
2362 --> animals:8:1
2363 |
2364 6 | finch
2365 7 | gorilla
2366 8 | hippopotamus
2367 | ^^^^^^^^^^^^ primary
2368 9 | inchworm
2369 10 | jackrabbit
2370 |
2371 ::: animals:1:1
2372 |
2373 1 | aardvark
2374 | -------- secondary
2375 2 | beetle
2376 3 | canary
2377 |
2378 ",
2379 );
2380
2381 env.context(0);
2390 let diag = env
2391 .err()
2392 .secondary("animals", "7", "7", "secondary 7")
2393 .primary("animals", "9", "9", "primary 9")
2394 .secondary("animals", "3", "3", "secondary 3")
2395 .secondary("animals", "1", "1", "secondary 1")
2396 .primary("animals", "5", "5", "primary 5")
2397 .build();
2398 insta::assert_snapshot!(
2399 env.render(&diag),
2400 @"
2401 error[test-diagnostic]: main diagnostic message
2402 --> animals:5:1
2403 |
2404 5 | elephant
2405 | ^^^^^^^^ primary 5
2406 |
2407 ::: animals:9:1
2408 |
2409 9 | inchworm
2410 | ^^^^^^^^ primary 9
2411 |
2412 ::: animals:1:1
2413 |
2414 1 | aardvark
2415 | -------- secondary 1
2416 |
2417 ::: animals:3:1
2418 |
2419 3 | canary
2420 | ------ secondary 3
2421 |
2422 ::: animals:7:1
2423 |
2424 7 | gorilla
2425 | ------- secondary 7
2426 ",
2427 );
2428 }
2429
2430 #[test]
2431 fn annotation_many_files_primary_always_comes_first() {
2432 let mut env = TestEnvironment::new();
2433 env.add("animals", ANIMALS);
2434 env.add("fruits", FRUITS);
2435
2436 let diag = env
2437 .err()
2438 .secondary("animals", "1", "1", "secondary")
2439 .primary("fruits", "1", "1", "primary")
2440 .build();
2441 insta::assert_snapshot!(
2442 env.render(&diag),
2443 @"
2444 error[test-diagnostic]: main diagnostic message
2445 --> fruits:1:1
2446 |
2447 1 | apple
2448 | ^^^^^ primary
2449 2 | banana
2450 3 | cantaloupe
2451 |
2452 ::: animals:1:1
2453 |
2454 1 | aardvark
2455 | -------- secondary
2456 2 | beetle
2457 3 | canary
2458 |
2459 ",
2460 );
2461
2462 env.context(0);
2467 let diag = env
2468 .err()
2469 .secondary("animals", "7", "7", "secondary animals 7")
2470 .secondary("fruits", "2", "2", "secondary fruits 2")
2471 .secondary("animals", "3", "3", "secondary animals 3")
2472 .secondary("animals", "1", "1", "secondary animals 1")
2473 .primary("animals", "11", "11", "primary animals 11")
2474 .primary("fruits", "10", "10", "primary fruits 10")
2475 .build();
2476 insta::assert_snapshot!(
2477 env.render(&diag),
2478 @"
2479 error[test-diagnostic]: main diagnostic message
2480 --> animals:11:1
2481 |
2482 11 | kangaroo
2483 | ^^^^^^^^ primary animals 11
2484 |
2485 ::: animals:1:1
2486 |
2487 1 | aardvark
2488 | -------- secondary animals 1
2489 |
2490 ::: animals:3:1
2491 |
2492 3 | canary
2493 | ------ secondary animals 3
2494 |
2495 ::: animals:7:1
2496 |
2497 7 | gorilla
2498 | ------- secondary animals 7
2499 |
2500 ::: fruits:10:1
2501 |
2502 10 | watermelon
2503 | ^^^^^^^^^^ primary fruits 10
2504 |
2505 ::: fruits:2:1
2506 |
2507 2 | banana
2508 | ------ secondary fruits 2
2509 ",
2510 );
2511 }
2512
2513 #[test]
2514 fn diagnostics_with_equal_locations_sort_by_concise_message() {
2515 let mut env = TestEnvironment::new();
2516 env.add("fruits", FRUITS);
2517 let mut diagnostics = [
2518 env.invalid_syntax("checking mod.py")
2519 .primary("fruits", "1", "1", "")
2520 .build(),
2521 env.invalid_syntax("checking main.py")
2522 .primary("fruits", "1", "1", "")
2523 .build(),
2524 ];
2525
2526 diagnostics.sort_by(|left, right| {
2527 left.rendering_sort_key(&env.db)
2528 .cmp(&right.rendering_sort_key(&env.db))
2529 });
2530
2531 assert_eq!(
2532 diagnostics
2533 .iter()
2534 .map(Diagnostic::headline_message)
2535 .collect::<Vec<_>>(),
2536 ["checking main.py", "checking mod.py"]
2537 );
2538 }
2539
2540 pub(super) struct TestEnvironment {
2543 db: TestDb,
2544 config: DisplayDiagnosticConfig,
2545 }
2546
2547 impl TestEnvironment {
2548 pub(super) fn new() -> TestEnvironment {
2552 let mut env = TestEnvironment {
2553 db: TestDb::new(),
2554 config: DisplayDiagnosticConfig::new("ty"),
2555 };
2556 env.merge_window(0);
2559 env
2560 }
2561
2562 pub(super) fn context(&mut self, lines: usize) {
2565 let config = self.config.clone();
2570 self.config = config.context(lines);
2571 }
2572
2573 pub(super) fn merge_window(&mut self, lines: usize) {
2578 let config = self.config.clone();
2579 self.config = config.merge_window(lines);
2580 }
2581
2582 pub(super) fn format(&mut self, format: DiagnosticFormat) {
2584 let config = self.config.clone();
2585 self.config = config.format(format);
2586 }
2587
2588 #[allow(
2590 dead_code,
2591 reason = "This is currently only used for JSON but will be needed soon for other formats"
2592 )]
2593 pub(super) fn preview(&mut self, yes: bool) {
2594 let config = self.config.clone();
2595 self.config = config.preview(yes);
2596 }
2597
2598 pub(super) fn hide_severity(&mut self, yes: bool) {
2600 let config = self.config.clone();
2601 self.config = config.hide_severity(yes);
2602 }
2603
2604 pub(super) fn show_fix_status(&mut self, yes: bool) {
2606 let config = self.config.clone();
2607 self.config = config.with_show_fix_status(yes);
2608 }
2609
2610 pub(super) fn fix_applicability(&mut self, applicability: Applicability) {
2612 let config = self.config.clone();
2613 self.config = config.with_fix_applicability(applicability);
2614 }
2615
2616 pub(super) fn add(&mut self, path: &str, contents: &str) {
2618 let path = SystemPath::new(path);
2619 self.db.write_file(path, contents).unwrap();
2620 }
2621
2622 fn span(&self, path: &str, line_offset_start: &str, line_offset_end: &str) -> Span {
2637 let span = self.path(path);
2638
2639 let file = span.expect_ty_file();
2640 let text = source_text(&self.db, file);
2641 let line_index = line_index(&self.db, file);
2642 let source = SourceCode::new(text.as_str(), &line_index);
2643
2644 let (line_start, offset_start) = parse_line_offset(line_offset_start);
2645 let (line_end, offset_end) = parse_line_offset(line_offset_end);
2646
2647 let start = match offset_start {
2648 None => source.line_start(line_start),
2649 Some(offset) => source.line_start(line_start) + offset,
2650 };
2651 let end = match offset_end {
2652 None => source.line_end(line_end) - TextSize::from(1),
2653 Some(offset) => source.line_start(line_end) + offset,
2654 };
2655 span.with_range(TextRange::new(start, end))
2656 }
2657
2658 pub(super) fn path(&self, path: &str) -> Span {
2660 let file = system_path_to_file(&self.db, path).unwrap();
2661 Span::from(file)
2662 }
2663
2664 pub(super) fn err(&mut self) -> DiagnosticBuilder<'_> {
2668 self.builder(
2669 "test-diagnostic",
2670 Severity::Error,
2671 "main diagnostic message",
2672 )
2673 }
2674
2675 fn sub_warn(&mut self) -> SubDiagnosticBuilder<'_> {
2679 self.sub_builder(SubDiagnosticSeverity::Warning, "sub-diagnostic message")
2680 }
2681
2682 pub(super) fn builder(
2684 &mut self,
2685 identifier: &'static str,
2686 severity: Severity,
2687 message: &str,
2688 ) -> DiagnosticBuilder<'_> {
2689 let diag = Diagnostic::new(id(identifier), severity, message);
2690 DiagnosticBuilder { env: self, diag }
2691 }
2692
2693 fn invalid_syntax(&mut self, message: &str) -> DiagnosticBuilder<'_> {
2695 let diag = Diagnostic::new(DiagnosticId::InvalidSyntax, Severity::Error, message);
2696 DiagnosticBuilder { env: self, diag }
2697 }
2698
2699 fn sub_builder(
2701 &mut self,
2702 severity: SubDiagnosticSeverity,
2703 message: &str,
2704 ) -> SubDiagnosticBuilder<'_> {
2705 let subdiag = SubDiagnostic::new(severity, message);
2706 SubDiagnosticBuilder { env: self, subdiag }
2707 }
2708
2709 pub(super) fn render(&self, diag: &Diagnostic) -> String {
2713 diag.display(&self.db, &self.config).to_string()
2714 }
2715
2716 pub(super) fn render_diagnostics(&self, diagnostics: &[Diagnostic]) -> String {
2722 DisplayDiagnostics::new(&self.db, &self.config, diagnostics).to_string()
2723 }
2724 }
2725
2726 pub(super) struct DiagnosticBuilder<'e> {
2733 env: &'e mut TestEnvironment,
2734 diag: Diagnostic,
2735 }
2736
2737 impl<'e> DiagnosticBuilder<'e> {
2738 pub(super) fn build(self) -> Diagnostic {
2740 self.diag
2741 }
2742
2743 pub(super) fn primary(
2751 mut self,
2752 path: &str,
2753 line_offset_start: &str,
2754 line_offset_end: &str,
2755 label: &str,
2756 ) -> DiagnosticBuilder<'e> {
2757 let span = self.env.span(path, line_offset_start, line_offset_end);
2758 let mut ann = Annotation::primary(span);
2759 if !label.is_empty() {
2760 ann = ann.message(label);
2761 }
2762 self.diag.annotate(ann);
2763 self
2764 }
2765
2766 pub(super) fn secondary(
2774 mut self,
2775 path: &str,
2776 line_offset_start: &str,
2777 line_offset_end: &str,
2778 label: &str,
2779 ) -> DiagnosticBuilder<'e> {
2780 let span = self.env.span(path, line_offset_start, line_offset_end);
2781 let mut ann = Annotation::secondary(span);
2782 if !label.is_empty() {
2783 ann = ann.message(label);
2784 }
2785 self.diag.annotate(ann);
2786 self
2787 }
2788
2789 fn secondary_code(mut self, secondary_code: &str) -> DiagnosticBuilder<'e> {
2791 self.diag
2792 .set_secondary_code(SecondaryCode::new(secondary_code.to_string()));
2793 self
2794 }
2795
2796 fn fix(mut self, fix: Fix) -> DiagnosticBuilder<'e> {
2798 self.diag.set_fix(fix);
2799 self
2800 }
2801
2802 fn noqa_offset(mut self, noqa_offset: TextSize) -> DiagnosticBuilder<'e> {
2804 self.diag.set_noqa_offset(noqa_offset);
2805 self
2806 }
2807
2808 pub(super) fn help(mut self, message: impl IntoDiagnosticMessage) -> DiagnosticBuilder<'e> {
2810 self.diag.help(message);
2811 self
2812 }
2813
2814 fn sub(
2816 mut self,
2817 f: impl Fn(&mut TestEnvironment) -> SubDiagnostic,
2818 ) -> DiagnosticBuilder<'e> {
2819 let sub = f(self.env);
2820 self.diag.sub(sub);
2821 self
2822 }
2823
2824 pub(super) fn documentation_url(mut self, url: impl Into<String>) -> DiagnosticBuilder<'e> {
2826 self.diag.set_documentation_url(Some(url.into()));
2827 self
2828 }
2829 }
2830
2831 struct SubDiagnosticBuilder<'e> {
2838 env: &'e mut TestEnvironment,
2839 subdiag: SubDiagnostic,
2840 }
2841
2842 impl<'e> SubDiagnosticBuilder<'e> {
2843 fn build(self) -> SubDiagnostic {
2845 self.subdiag
2846 }
2847
2848 fn primary(
2856 mut self,
2857 path: &str,
2858 line_offset_start: &str,
2859 line_offset_end: &str,
2860 label: &str,
2861 ) -> SubDiagnosticBuilder<'e> {
2862 let span = self.env.span(path, line_offset_start, line_offset_end);
2863 let mut ann = Annotation::primary(span);
2864 if !label.is_empty() {
2865 ann = ann.message(label);
2866 }
2867 self.subdiag.annotate(ann);
2868 self
2869 }
2870
2871 fn secondary(
2879 mut self,
2880 path: &str,
2881 line_offset_start: &str,
2882 line_offset_end: &str,
2883 label: &str,
2884 ) -> SubDiagnosticBuilder<'e> {
2885 let span = self.env.span(path, line_offset_start, line_offset_end);
2886 let mut ann = Annotation::secondary(span);
2887 if !label.is_empty() {
2888 ann = ann.message(label);
2889 }
2890 self.subdiag.annotate(ann);
2891 self
2892 }
2893 }
2894
2895 fn id(lint_name: &'static str) -> DiagnosticId {
2896 DiagnosticId::lint(lint_name)
2897 }
2898
2899 fn parse_line_offset(s: &str) -> (OneIndexed, Option<TextSize>) {
2900 let Some((line, offset)) = s.split_once(":") else {
2901 let line_number = OneIndexed::new(s.parse().unwrap()).unwrap();
2902 return (line_number, None);
2903 };
2904 let line_number = OneIndexed::new(line.parse().unwrap()).unwrap();
2905 let offset = TextSize::from(offset.parse::<u32>().unwrap());
2906 (line_number, Some(offset))
2907 }
2908
2909 pub(crate) fn create_diagnostics(
2911 format: DiagnosticFormat,
2912 ) -> (TestEnvironment, Vec<Diagnostic>) {
2913 let mut env = TestEnvironment::new();
2914 env.add(
2915 "fib.py",
2916 r#"import os
2917
2918
2919def fibonacci(n):
2920 """Compute the nth number in the Fibonacci sequence."""
2921 x = 1
2922 if n == 0:
2923 return 0
2924 elif n == 1:
2925 return 1
2926 else:
2927 return fibonaccii(n - 1) + fibonacci(n - 2)
2928"#,
2929 );
2930 env.add("undef.py", r"if a == 1: pass");
2931 env.format(format);
2932
2933 let diagnostics = vec![
2934 env.builder("unused-import", Severity::Error, "`os` imported but unused")
2935 .primary("fib.py", "1:7", "1:9", "")
2936 .help("Remove unused import: `os`")
2937 .secondary_code("F401")
2938 .fix(Fix::unsafe_edit(Edit::range_deletion(TextRange::new(
2939 TextSize::from(0),
2940 TextSize::from(10),
2941 ))))
2942 .noqa_offset(TextSize::from(7))
2943 .documentation_url("https://docs.astral.sh/ruff/rules/unused-import")
2944 .build(),
2945 env.builder(
2946 "unused-variable",
2947 Severity::Error,
2948 "Local variable `x` is assigned to but never used",
2949 )
2950 .primary("fib.py", "6:4", "6:5", "")
2951 .help("Remove assignment to unused variable `x`")
2952 .secondary_code("F841")
2953 .fix(Fix::unsafe_edit(Edit::deletion(
2954 TextSize::from(94),
2955 TextSize::from(99),
2956 )))
2957 .noqa_offset(TextSize::from(94))
2958 .documentation_url("https://docs.astral.sh/ruff/rules/unused-variable")
2959 .build(),
2960 env.builder("undefined-name", Severity::Error, "Undefined name `a`")
2961 .primary("undef.py", "1:3", "1:4", "")
2962 .secondary_code("F821")
2963 .noqa_offset(TextSize::from(3))
2964 .documentation_url("https://docs.astral.sh/ruff/rules/undefined-name")
2965 .build(),
2966 env.builder(
2967 "undefined-name",
2968 Severity::Error,
2969 "Undefined name `fibonaccii`",
2970 )
2971 .primary("fib.py", "12:15", "12:25", "")
2972 .secondary_code("F821")
2973 .noqa_offset(ruff_text_size::TextSize::from(0))
2974 .documentation_url("https://docs.astral.sh/ruff/rules/undefined-name")
2975 .secondary("fib.py", "12:35", "12:36", "")
2976 .sub(|env| {
2977 env.sub_builder(
2978 SubDiagnosticSeverity::Info,
2979 "Did you mean to import it from `/some/path/def.py`?",
2980 )
2981 .primary("fib.py", "4:4", "4:13", "`fibonacci` is defined here")
2982 .secondary("fib.py", "5:4", "5", "`fibonacci` is documented here")
2983 .build()
2984 })
2985 .build(),
2986 ];
2987
2988 (env, diagnostics)
2989 }
2990
2991 pub(crate) fn create_syntax_error_diagnostics(
2993 format: DiagnosticFormat,
2994 ) -> (TestEnvironment, Vec<Diagnostic>) {
2995 let mut env = TestEnvironment::new();
2996 env.add(
2997 "syntax_errors.py",
2998 r"from os import
2999
3000if call(foo
3001 def bar():
3002 pass
3003",
3004 );
3005 env.format(format);
3006
3007 let diagnostics = vec![
3008 env.invalid_syntax("Expected one or more symbol names after import")
3009 .primary("syntax_errors.py", "1:14", "1:15", "")
3010 .build(),
3011 env.invalid_syntax("Expected ')', found newline")
3012 .primary("syntax_errors.py", "3:11", "3:12", "")
3013 .build(),
3014 ];
3015
3016 (env, diagnostics)
3017 }
3018
3019 pub(super) static NOTEBOOK: &str = r##"
3041 {
3042 "cells": [
3043 {
3044 "cell_type": "code",
3045 "metadata": {},
3046 "outputs": [],
3047 "source": [
3048 "# cell 1\n",
3049 "import os"
3050 ]
3051 },
3052 {
3053 "cell_type": "code",
3054 "metadata": {},
3055 "outputs": [],
3056 "source": [
3057 "# cell 2\n",
3058 "import math\n",
3059 "\n",
3060 "print('hello world')"
3061 ]
3062 },
3063 {
3064 "cell_type": "code",
3065 "metadata": {},
3066 "outputs": [],
3067 "source": [
3068 "# cell 3\n",
3069 "def foo():\n",
3070 " print()\n",
3071 " x = 1\n"
3072 ]
3073 }
3074 ],
3075 "metadata": {},
3076 "nbformat": 4,
3077 "nbformat_minor": 5
3078}
3079"##;
3080
3081 pub(crate) fn create_notebook_diagnostics(
3083 format: DiagnosticFormat,
3084 ) -> (TestEnvironment, Vec<Diagnostic>) {
3085 let mut env = TestEnvironment::new();
3086 env.add("notebook.ipynb", NOTEBOOK);
3087 env.format(format);
3088
3089 let diagnostics = vec![
3090 env.builder("unused-import", Severity::Error, "`os` imported but unused")
3091 .primary("notebook.ipynb", "2:7", "2:9", "")
3092 .help("Remove unused import: `os`")
3093 .secondary_code("F401")
3094 .fix(Fix::safe_edit(Edit::range_deletion(TextRange::new(
3095 TextSize::from(9),
3096 TextSize::from(19),
3097 ))))
3098 .noqa_offset(TextSize::from(16))
3099 .documentation_url("https://docs.astral.sh/ruff/rules/unused-import")
3100 .build(),
3101 env.builder(
3102 "unused-import",
3103 Severity::Error,
3104 "`math` imported but unused",
3105 )
3106 .primary("notebook.ipynb", "4:7", "4:11", "")
3107 .help("Remove unused import: `math`")
3108 .secondary_code("F401")
3109 .fix(Fix::safe_edit(Edit::range_deletion(TextRange::new(
3110 TextSize::from(28),
3111 TextSize::from(40),
3112 ))))
3113 .noqa_offset(TextSize::from(35))
3114 .documentation_url("https://docs.astral.sh/ruff/rules/unused-import")
3115 .build(),
3116 env.builder(
3117 "unused-variable",
3118 Severity::Error,
3119 "Local variable `x` is assigned to but never used",
3120 )
3121 .primary("notebook.ipynb", "10:4", "10:5", "")
3122 .help("Remove assignment to unused variable `x`")
3123 .secondary_code("F841")
3124 .fix(Fix::unsafe_edit(Edit::range_deletion(TextRange::new(
3125 TextSize::from(94),
3126 TextSize::from(104),
3127 ))))
3128 .noqa_offset(TextSize::from(98))
3129 .documentation_url("https://docs.astral.sh/ruff/rules/unused-variable")
3130 .build(),
3131 ];
3132
3133 (env, diagnostics)
3134 }
3135}