use std::borrow::Cow;
use std::collections::BTreeMap;
use std::path::Path;
use annotate_snippets::{
Annotation as AnnotateAnnotation, AnnotationKind, Group as AnnotateGroup,
Level as AnnotateLevel, Snippet as AnnotateSnippet,
};
use full::FullRenderer;
use ruff_notebook::{Notebook, NotebookIndex};
use ruff_source_file::{LineIndex, OneIndexed, SourceCode};
use ruff_text_size::{TextLen, TextRange, TextSize};
use crate::{
Db,
files::File,
source::{SourceText, line_index, source_text},
};
use super::{
Annotation, Diagnostic, DiagnosticFormat, DiagnosticSource, DisplayDiagnosticConfig,
SubDiagnostic, UnifiedFile,
};
use azure::AzureRenderer;
use concise::ConciseRenderer;
use github::GithubRenderer;
use pylint::PylintRenderer;
mod azure;
mod concise;
mod full;
pub mod github;
#[cfg(feature = "serde")]
mod gitlab;
#[cfg(feature = "serde")]
mod json;
#[cfg(feature = "serde")]
mod json_lines;
#[cfg(feature = "junit")]
mod junit;
mod pylint;
#[cfg(feature = "serde")]
mod rdjson;
pub struct DisplayDiagnostic<'a> {
config: &'a DisplayDiagnosticConfig,
resolver: &'a dyn FileResolver,
diag: &'a Diagnostic,
}
impl<'a> DisplayDiagnostic<'a> {
pub(crate) fn new(
resolver: &'a dyn FileResolver,
config: &'a DisplayDiagnosticConfig,
diag: &'a Diagnostic,
) -> DisplayDiagnostic<'a> {
DisplayDiagnostic {
config,
resolver,
diag,
}
}
}
impl std::fmt::Display for DisplayDiagnostic<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
DisplayDiagnostics::new(self.resolver, self.config, std::slice::from_ref(self.diag)).fmt(f)
}
}
pub struct DisplayDiagnostics<'a> {
config: &'a DisplayDiagnosticConfig,
resolver: &'a dyn FileResolver,
diagnostics: &'a [Diagnostic],
}
impl<'a> DisplayDiagnostics<'a> {
pub fn new(
resolver: &'a dyn FileResolver,
config: &'a DisplayDiagnosticConfig,
diagnostics: &'a [Diagnostic],
) -> DisplayDiagnostics<'a> {
DisplayDiagnostics {
config,
resolver,
diagnostics,
}
}
}
impl std::fmt::Display for DisplayDiagnostics<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self.config.format {
DiagnosticFormat::Concise => {
ConciseRenderer::new(self.resolver, self.config).render(f, self.diagnostics)?;
}
DiagnosticFormat::Full => {
FullRenderer::new(self.resolver, self.config).render(f, self.diagnostics)?;
}
DiagnosticFormat::Azure => {
AzureRenderer::new(self.resolver, self.config).render(f, self.diagnostics)?;
}
#[cfg(feature = "serde")]
DiagnosticFormat::Json => {
json::JsonRenderer::new(self.resolver, self.config).render(f, self.diagnostics)?;
}
#[cfg(feature = "serde")]
DiagnosticFormat::JsonLines => {
json_lines::JsonLinesRenderer::new(self.resolver, self.config)
.render(f, self.diagnostics)?;
}
#[cfg(feature = "serde")]
DiagnosticFormat::Rdjson => {
rdjson::RdjsonRenderer::new(self.resolver, self.config)
.render(f, self.diagnostics)?;
}
DiagnosticFormat::Pylint => {
PylintRenderer::new(self.resolver, self.config).render(f, self.diagnostics)?;
}
#[cfg(feature = "junit")]
DiagnosticFormat::Junit => {
junit::JunitRenderer::new(self.resolver, self.config)
.render(f, self.diagnostics)?;
}
#[cfg(feature = "serde")]
DiagnosticFormat::Gitlab => {
gitlab::GitlabRenderer::new(self.resolver, self.config)
.render(f, self.diagnostics)?;
}
DiagnosticFormat::Github => {
GithubRenderer::new(self.resolver, self.config).render(f, self.diagnostics)?;
}
}
Ok(())
}
}
#[derive(Debug)]
struct Resolved<'a> {
diagnostics: Vec<ResolvedDiagnostic<'a>>,
}
impl<'a> Resolved<'a> {
fn new(
resolver: &'a dyn FileResolver,
diag: &'a Diagnostic,
config: &DisplayDiagnosticConfig,
) -> Resolved<'a> {
let mut diagnostics = vec![];
diagnostics.push(ResolvedDiagnostic::from_diagnostic(resolver, config, diag));
for sub in &diag.inner.subs {
diagnostics.push(ResolvedDiagnostic::from_sub_diagnostic(resolver, sub));
}
Resolved { diagnostics }
}
fn to_renderable(&self, config: &DisplayDiagnosticConfig) -> Renderable<'_> {
Renderable {
diagnostics: self
.diagnostics
.iter()
.map(|diag| diag.to_renderable(config))
.collect(),
}
}
}
#[derive(Debug)]
struct ResolvedDiagnostic<'a> {
level: AnnotateLevel<'static>,
id: Option<String>,
documentation_url: Option<String>,
message: String,
annotations: Vec<ResolvedAnnotation<'a>>,
is_fixable: bool,
header_offset: usize,
}
impl<'a> ResolvedDiagnostic<'a> {
fn from_diagnostic(
resolver: &'a dyn FileResolver,
config: &DisplayDiagnosticConfig,
diag: &'a Diagnostic,
) -> ResolvedDiagnostic<'a> {
let annotations: Vec<_> = diag
.inner
.annotations
.iter()
.filter_map(|ann| {
let path = ann
.span
.file
.relative_path(resolver)
.to_str()
.unwrap_or_else(|| ann.span.file.path(resolver));
let diagnostic_source = ann.span.file.diagnostic_source(resolver);
ResolvedAnnotation::new(path, &diagnostic_source, ann, resolver)
})
.collect();
let use_code = !config.preview || config.prefer_rule_codes;
let id = if use_code && let Some(code) = diag.secondary_code() {
code.to_string()
} else if config.hide_severity {
format!("{id}:", id = diag.id())
} else {
diag.id().to_string()
};
let level = diag.inner.severity.to_annotate();
let level = if config.hide_severity {
level.no_name()
} else {
level
};
ResolvedDiagnostic {
level,
id: Some(id),
documentation_url: diag.documentation_url().map(ToString::to_string),
message: diag.inner.message.as_str().to_string(),
annotations,
is_fixable: config.show_fix_status
&& diag.has_applicable_fix(config.fix_applicability()),
header_offset: diag.inner.header_offset,
}
}
fn from_sub_diagnostic(
resolver: &'a dyn FileResolver,
diag: &'a SubDiagnostic,
) -> ResolvedDiagnostic<'a> {
let annotations: Vec<_> = diag
.inner
.annotations
.iter()
.filter_map(|ann| {
let path = ann
.span
.file
.relative_path(resolver)
.to_str()
.unwrap_or_else(|| ann.span.file.path(resolver));
let diagnostic_source = ann.span.file.diagnostic_source(resolver);
ResolvedAnnotation::new(path, &diagnostic_source, ann, resolver)
})
.collect();
ResolvedDiagnostic {
level: diag.inner.severity.to_annotate(),
id: None,
documentation_url: None,
message: diag.inner.message.as_str().to_string(),
annotations,
is_fixable: false,
header_offset: 0,
}
}
fn to_renderable<'r>(&'r self, config: &DisplayDiagnosticConfig) -> RenderableDiagnostic<'r> {
let mut ann_by_path: BTreeMap<&'a str, Vec<&ResolvedAnnotation<'a>>> = BTreeMap::new();
for ann in &self.annotations {
ann_by_path.entry(ann.path).or_default().push(ann);
}
for anns in ann_by_path.values_mut() {
anns.sort_by_key(|ann1| ann1.range.start());
}
let merge_window = config.merge_window.max(config.context);
let mut snippet_by_path: BTreeMap<&'a str, Vec<Vec<&ResolvedAnnotation<'a>>>> =
BTreeMap::new();
for (path, anns) in ann_by_path {
let mut snippet = vec![];
for ann in anns {
let Some(prev) = snippet.last() else {
snippet.push(ann);
continue;
};
let prev_context_ends = context_after(
&prev.diagnostic_source.as_source_code(),
merge_window,
prev.line_end,
prev.notebook_index.as_ref(),
)
.get();
let this_context_begins = context_before(
&ann.diagnostic_source.as_source_code(),
merge_window,
ann.line_start,
ann.notebook_index.as_ref(),
)
.get();
let prev_cell_index = prev.notebook_index.as_ref().map(|notebook_index| {
let prev_end = prev
.diagnostic_source
.as_source_code()
.line_column(prev.range.end());
notebook_index.cell(prev_end.line).unwrap_or_default().get()
});
let this_cell_index = ann.notebook_index.as_ref().map(|notebook_index| {
let this_start = ann
.diagnostic_source
.as_source_code()
.line_column(ann.range.start());
notebook_index
.cell(this_start.line)
.unwrap_or_default()
.get()
});
let in_different_cells = prev_cell_index != this_cell_index;
if in_different_cells || this_context_begins.saturating_sub(prev_context_ends) > 1 {
snippet_by_path
.entry(path)
.or_default()
.push(std::mem::take(&mut snippet));
}
snippet.push(ann);
}
if !snippet.is_empty() {
snippet_by_path.entry(path).or_default().push(snippet);
}
}
let mut snippets_by_input = vec![];
for (path, snippets) in snippet_by_path {
snippets_by_input.push(RenderableSnippets::new(config.context, path, &snippets));
}
snippets_by_input
.sort_by(|snips1, snips2| snips1.has_primary.cmp(&snips2.has_primary).reverse());
RenderableDiagnostic {
level: self.level.clone(),
id: self.id.as_deref(),
documentation_url: self.documentation_url.as_deref(),
message: &self.message,
snippets_by_input,
is_fixable: self.is_fixable,
header_offset: self.header_offset,
}
}
}
#[derive(Debug)]
struct ResolvedAnnotation<'a> {
path: &'a str,
diagnostic_source: DiagnosticSource,
range: TextRange,
line_start: OneIndexed,
line_end: OneIndexed,
message: Option<&'a str>,
is_primary: bool,
hide_snippet: bool,
notebook_index: Option<NotebookIndex>,
}
impl<'a> ResolvedAnnotation<'a> {
fn new(
path: &'a str,
diagnostic_source: &DiagnosticSource,
ann: &'a Annotation,
resolver: &'a dyn FileResolver,
) -> Option<ResolvedAnnotation<'a>> {
let source = diagnostic_source.as_source_code();
let (range, line_start, line_end) = match (ann.span.range(), ann.message.is_some()) {
(None, _) => (
TextRange::empty(TextSize::new(0)),
OneIndexed::MIN,
OneIndexed::MIN,
),
(Some(range), _) => {
let line_start = source.line_index(range.start());
let mut line_end = source.line_index(range.end());
if source.slice(range).ends_with(['\r', '\n']) {
line_end = line_end.saturating_sub(1).max(line_start);
}
(range, line_start, line_end)
}
};
Some(ResolvedAnnotation {
path,
diagnostic_source: diagnostic_source.clone(),
range,
line_start,
line_end,
message: ann.get_message(),
is_primary: ann.is_primary,
hide_snippet: ann.hide_snippet,
notebook_index: resolver.notebook_index(&ann.span.file),
})
}
}
#[derive(Debug)]
struct Renderable<'r> {
diagnostics: Vec<RenderableDiagnostic<'r>>,
}
#[derive(Debug)]
struct RenderableDiagnostic<'r> {
level: AnnotateLevel<'static>,
id: Option<&'r str>,
documentation_url: Option<&'r str>,
message: &'r str,
snippets_by_input: Vec<RenderableSnippets<'r>>,
is_fixable: bool,
header_offset: usize,
}
impl RenderableDiagnostic<'_> {
fn to_annotate(&self) -> AnnotateGroup<'_> {
let snippets = self.snippets_by_input.iter().flat_map(|snippets| {
let path = snippets.path;
snippets
.snippets
.iter()
.map(|snippet| snippet.to_annotate(path))
});
let mut title = self
.level
.clone()
.primary_title(self.message)
.is_fixable(self.is_fixable);
if let Some(id) = self.id {
title = title.id(id);
if let Some(url) = self.documentation_url {
title = title.id_url(url);
}
}
title.elements(snippets).lineno_offset(self.header_offset)
}
}
#[derive(Debug)]
struct RenderableSnippets<'r> {
path: &'r str,
snippets: Vec<RenderableSnippet<'r>>,
has_primary: bool,
}
impl<'r> RenderableSnippets<'r> {
fn new<'a>(
context: usize,
path: &'r str,
resolved_snippets: &'a [Vec<&'r ResolvedAnnotation<'r>>],
) -> RenderableSnippets<'r> {
assert!(!resolved_snippets.is_empty());
let mut has_primary = false;
let mut snippets = vec![];
for anns in resolved_snippets {
let snippet = RenderableSnippet::new(context, anns);
has_primary = has_primary || snippet.has_primary;
snippets.push(snippet);
}
snippets.sort_by(|s1, s2| s1.has_primary.cmp(&s2.has_primary).reverse());
RenderableSnippets {
path,
snippets,
has_primary,
}
}
}
#[derive(Debug)]
struct RenderableSnippet<'r> {
snippet: Cow<'r, str>,
line_start: OneIndexed,
annotations: Vec<RenderableAnnotation<'r>>,
has_primary: bool,
cell_index: Option<usize>,
}
impl<'r> RenderableSnippet<'r> {
fn new<'a>(context: usize, anns: &'a [&'r ResolvedAnnotation<'r>]) -> RenderableSnippet<'r> {
assert!(
!anns.is_empty(),
"creating a renderable snippet requires a non-zero number of annotations",
);
let diagnostic_source = &anns[0].diagnostic_source;
let notebook_index = anns[0].notebook_index.as_ref();
let source = diagnostic_source.as_source_code();
let has_primary = anns.iter().any(|ann| ann.is_primary);
let content_start_index = anns.iter().map(|ann| ann.line_start).min().unwrap();
let line_start = context_before(&source, context, content_start_index, notebook_index);
let start = source.line_column(anns[0].range.start());
let cell_index = notebook_index
.map(|notebook_index| notebook_index.cell(start.line).unwrap_or_default().get());
let content_end_index = anns.iter().map(|ann| ann.line_end).max().unwrap();
let line_end = context_after(&source, context, content_end_index, notebook_index);
let snippet_start = source.line_start(line_start);
let snippet_end = source.line_end(line_end);
let snippet = diagnostic_source
.as_source_code()
.slice(TextRange::new(snippet_start, snippet_end));
const BOM: char = '\u{feff}';
let bom_len = BOM.text_len();
let (snippet, snippet_start) =
if snippet_start == TextSize::ZERO && snippet.starts_with(BOM) {
(
&snippet[bom_len.to_usize()..],
snippet_start + TextSize::new(bom_len.to_u32()),
)
} else {
(snippet, snippet_start)
};
let annotations = anns
.iter()
.map(|ann| RenderableAnnotation::new(snippet_start, ann))
.collect();
let EscapedSourceCode {
text: snippet,
annotations,
} = replace_unprintable(snippet, annotations).fix_up_empty_spans_after_line_terminator();
let line_start = notebook_index.map_or(line_start, |notebook_index| {
notebook_index
.cell_row(line_start)
.unwrap_or(OneIndexed::MIN)
});
RenderableSnippet {
snippet,
line_start,
annotations,
has_primary,
cell_index,
}
}
fn to_annotate<'a>(&'a self, path: &'a str) -> AnnotateSnippet<'a, AnnotateAnnotation<'a>> {
AnnotateSnippet::source(self.snippet.as_ref())
.path(path)
.line_start(self.line_start.get())
.fold(false)
.annotations(
self.annotations
.iter()
.map(RenderableAnnotation::to_annotate),
)
.cell_index(self.cell_index)
}
}
#[derive(Debug)]
struct RenderableAnnotation<'r> {
range: TextRange,
message: Option<&'r str>,
is_primary: bool,
hide_snippet: bool,
}
impl<'r> RenderableAnnotation<'r> {
fn new(snippet_start: TextSize, ann: &'_ ResolvedAnnotation<'r>) -> RenderableAnnotation<'r> {
let range = ann.range.checked_sub(snippet_start).unwrap_or(ann.range);
RenderableAnnotation {
range,
message: ann.message,
is_primary: ann.is_primary,
hide_snippet: ann.hide_snippet,
}
}
fn to_annotate(&self) -> AnnotateAnnotation<'_> {
let kind = if self.is_primary {
AnnotationKind::Primary
} else {
AnnotationKind::Context
};
let mut ann = kind.span(self.range.into());
if let Some(message) = self.message {
ann = ann.label(message);
}
ann.hide_snippet(self.hide_snippet)
}
}
pub trait FileResolver {
fn path(&self, file: File) -> &str;
fn input(&self, file: File) -> Input;
fn notebook_index(&self, file: &UnifiedFile) -> Option<NotebookIndex>;
fn is_notebook(&self, file: &UnifiedFile) -> bool;
fn current_directory(&self) -> &Path;
}
impl<T> FileResolver for T
where
T: Db,
{
fn path(&self, file: File) -> &str {
file.path(self).as_str()
}
fn input(&self, file: File) -> Input {
Input {
text: source_text(self, file),
line_index: line_index(self, file),
}
}
fn notebook_index(&self, file: &UnifiedFile) -> Option<NotebookIndex> {
match file {
UnifiedFile::Ty(file) => self
.input(*file)
.text
.as_notebook()
.map(Notebook::index)
.cloned(),
UnifiedFile::Ruff(_) => unimplemented!("Expected an interned ty file"),
}
}
fn is_notebook(&self, file: &UnifiedFile) -> bool {
match file {
UnifiedFile::Ty(file) => self.input(*file).text.as_notebook().is_some(),
UnifiedFile::Ruff(_) => unimplemented!("Expected an interned ty file"),
}
}
fn current_directory(&self) -> &Path {
self.system().current_directory().as_std_path()
}
}
impl FileResolver for &dyn Db {
fn path(&self, file: File) -> &str {
file.path(*self).as_str()
}
fn input(&self, file: File) -> Input {
Input {
text: source_text(*self, file),
line_index: line_index(*self, file),
}
}
fn notebook_index(&self, file: &UnifiedFile) -> Option<NotebookIndex> {
match file {
UnifiedFile::Ty(file) => self
.input(*file)
.text
.as_notebook()
.map(Notebook::index)
.cloned(),
UnifiedFile::Ruff(_) => unimplemented!("Expected an interned ty file"),
}
}
fn is_notebook(&self, file: &UnifiedFile) -> bool {
match file {
UnifiedFile::Ty(file) => self.input(*file).text.as_notebook().is_some(),
UnifiedFile::Ruff(_) => unimplemented!("Expected an interned ty file"),
}
}
fn current_directory(&self) -> &Path {
self.system().current_directory().as_std_path()
}
}
#[derive(Clone, Debug)]
pub struct Input {
pub(crate) text: SourceText,
pub(crate) line_index: LineIndex,
}
fn context_before(
source: &SourceCode<'_, '_>,
len: usize,
start: OneIndexed,
notebook_index: Option<&NotebookIndex>,
) -> OneIndexed {
let mut line = start.saturating_sub(len);
while line < start {
if !source.line_text(line).trim().is_empty() {
break;
}
line = line.saturating_add(1);
}
if let Some(index) = notebook_index {
let content_start_cell = index.cell(start).unwrap_or(OneIndexed::MIN);
while line < start {
if index.cell(line).unwrap_or(OneIndexed::MIN) == content_start_cell {
break;
}
line = line.saturating_add(1);
}
}
line
}
fn context_after(
source: &SourceCode<'_, '_>,
len: usize,
start: OneIndexed,
notebook_index: Option<&NotebookIndex>,
) -> OneIndexed {
let max_lines = OneIndexed::from_zero_indexed(source.line_count());
let mut line = start.saturating_add(len).min(max_lines);
while line > start {
if !source.line_text(line).trim().is_empty() {
break;
}
line = line.saturating_sub(1);
}
if let Some(index) = notebook_index {
let content_end_cell = index.cell(start).unwrap_or(OneIndexed::MIN);
while line > start {
if index.cell(line).unwrap_or(OneIndexed::MIN) == content_end_cell {
break;
}
line = line.saturating_sub(1);
}
}
line
}
fn replace_unprintable<'r>(
source: &'r str,
mut annotations: Vec<RenderableAnnotation<'r>>,
) -> EscapedSourceCode<'r> {
let mut update_ranges = |index: usize, len: u32| {
for ann in &mut annotations {
if index < usize::from(ann.range.start()) {
ann.range += TextSize::new(len - 1);
} else if index < usize::from(ann.range.end()) {
ann.range = ann.range.add_end(TextSize::new(len - 1));
}
}
};
let unprintable_replacement = |c: char| -> Option<char> {
match c {
'\x07' => Some('␇'),
'\x08' => Some('␈'),
'\x1b' => Some('␛'),
'\x7f' => Some('␡'),
_ => None,
}
};
let mut last_end = 0;
let mut result = String::new();
for (index, c) in source.char_indices() {
if c == '\r' && !source[index + 1..].starts_with("\n") {
result.push_str(&source[last_end..index]);
result.push('\n');
last_end = index + 1;
} else if let Some(printable) = unprintable_replacement(c) {
result.push_str(&source[last_end..index]);
let len = printable.text_len().to_u32();
update_ranges(result.text_len().to_usize(), len);
result.push(printable);
last_end = index + 1;
}
}
if result.is_empty() {
EscapedSourceCode {
annotations,
text: Cow::Borrowed(source),
}
} else {
result.push_str(&source[last_end..]);
EscapedSourceCode {
annotations,
text: Cow::Owned(result),
}
}
}
struct EscapedSourceCode<'r> {
text: Cow<'r, str>,
annotations: Vec<RenderableAnnotation<'r>>,
}
impl<'r> EscapedSourceCode<'r> {
fn fix_up_empty_spans_after_line_terminator(mut self) -> EscapedSourceCode<'r> {
for ann in &mut self.annotations {
let range = ann.range;
if !range.is_empty()
|| range.start() == TextSize::from(0)
|| range.start() >= self.text.text_len()
{
continue;
}
if !matches!(
self.text.as_bytes()[range.start().to_usize() - 1],
b'\n' | b'\r'
) {
continue;
}
let start = range.start();
let end =
TextSize::try_from(self.text.ceil_char_boundary(start.to_usize() + 1)).unwrap();
ann.range = TextRange::new(start, end);
}
self
}
}
pub struct DummyFileResolver;
impl FileResolver for DummyFileResolver {
fn path(&self, _file: File) -> &str {
unimplemented!()
}
fn input(&self, _file: File) -> Input {
unimplemented!()
}
fn notebook_index(&self, _file: &UnifiedFile) -> Option<NotebookIndex> {
None
}
fn is_notebook(&self, _file: &UnifiedFile) -> bool {
false
}
fn current_directory(&self) -> &Path {
Path::new(".")
}
}
#[cfg(test)]
mod tests {
use ruff_diagnostics::{Applicability, Edit, Fix};
use crate::diagnostic::{
Annotation, DiagnosticId, IntoDiagnosticMessage, SecondaryCode, Severity, Span,
SubDiagnosticSeverity,
};
use crate::files::system_path_to_file;
use crate::system::{DbWithWritableSystem, SystemPath};
use crate::tests::TestDb;
use super::*;
static ANIMALS: &str = "\
aardvark
beetle
canary
dog
elephant
finch
gorilla
hippopotamus
inchworm
jackrabbit
kangaroo
";
static SPACEY_ANIMALS: &str = "\
aardvark
beetle
canary
dog
elephant
finch
gorilla
hippopotamus
inchworm
jackrabbit
kangaroo
";
static FRUITS: &str = "\
apple
banana
cantaloupe
lime
orange
pear
raspberry
strawberry
tomato
watermelon
";
static NON_ASCII: &str = "\
☃☃☃☃☃☃☃☃☃☃☃☃
💩💩💩💩💩💩💩💩💩💩💩💩
ΔΔΔΔΔΔΔΔΔΔΔΔ
ββββββββββββ
ΣΣΣΣΣΣΣΣΣΣΣΣ
ξξξξξξξξξξξξ
ππππππππππππ
θθθθθθθθθθθθ
ΦΦΦΦΦΦΦΦΦΦΦΦ
λλλλλλλλλλλλ
";
#[test]
fn basic() {
let mut env = TestEnvironment::new();
env.add("animals", ANIMALS);
let diag = env.err().primary("animals", "5", "5", "").build();
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:5:1
|
3 | canary
4 | dog
5 | elephant
| ^^^^^^^^
6 | finch
7 | gorilla
|
",
);
let diag = env
.builder(
"test-diagnostic",
Severity::Warning,
"main diagnostic message",
)
.primary("animals", "5", "5", "")
.build();
insta::assert_snapshot!(
env.render(&diag),
@"
warning[test-diagnostic]: main diagnostic message
--> animals:5:1
|
3 | canary
4 | dog
5 | elephant
| ^^^^^^^^
6 | finch
7 | gorilla
|
",
);
let diag = env
.builder("test-diagnostic", Severity::Info, "main diagnostic message")
.primary("animals", "5", "5", "")
.build();
insta::assert_snapshot!(
env.render(&diag),
@"
info[test-diagnostic]: main diagnostic message
--> animals:5:1
|
3 | canary
4 | dog
5 | elephant
| ^^^^^^^^
6 | finch
7 | gorilla
|
",
);
}
#[test]
fn no_range() {
let mut env = TestEnvironment::new();
env.add("animals", ANIMALS);
let mut builder = env.err();
builder
.diag
.annotate(Annotation::primary(builder.env.path("animals")));
let diag = builder.build();
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:1:1
|
1 | aardvark
| ^
2 | beetle
3 | canary
|
",
);
let mut builder = env.err();
builder.diag.annotate(
Annotation::primary(builder.env.path("animals")).message("primary annotation message"),
);
let diag = builder.build();
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:1:1
|
1 | aardvark
| ^ primary annotation message
2 | beetle
3 | canary
|
",
);
}
#[test]
fn non_ascii() {
let mut env = TestEnvironment::new();
env.add("non-ascii", NON_ASCII);
let diag = env.err().primary("non-ascii", "5", "5", "").build();
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> non-ascii:5:1
|
3 | ΔΔΔΔΔΔΔΔΔΔΔΔ
4 | ββββββββββββ
5 | ΣΣΣΣΣΣΣΣΣΣΣΣ
| ^^^^^^^^^^^^
6 | ξξξξξξξξξξξξ
7 | ππππππππππππ
|
",
);
let diag = env.err().primary("non-ascii", "2:4", "2:8", "").build();
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> non-ascii:2:2
|
1 | ☃☃☃☃☃☃☃☃☃☃☃☃
2 | 💩💩💩💩💩💩💩💩💩💩💩💩
| ^^
3 | ΔΔΔΔΔΔΔΔΔΔΔΔ
4 | ββββββββββββ
|
",
);
}
#[test]
fn config_context() {
let mut env = TestEnvironment::new();
env.add("animals", ANIMALS);
let diag = env.err().primary("animals", "5", "5", "").build();
env.context(1);
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:5:1
|
4 | dog
5 | elephant
| ^^^^^^^^
6 | finch
|
",
);
let diag = env.err().primary("animals", "5", "5", "").build();
env.context(0);
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:5:1
|
5 | elephant
| ^^^^^^^^
",
);
let diag = env.err().primary("animals", "1", "1", "").build();
env.context(2);
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:1:1
|
1 | aardvark
| ^^^^^^^^
2 | beetle
3 | canary
|
",
);
let diag = env.err().primary("animals", "11", "11", "").build();
env.context(2);
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:11:1
|
9 | inchworm
10 | jackrabbit
11 | kangaroo
| ^^^^^^^^
",
);
let diag = env.err().primary("animals", "5", "5", "").build();
env.context(200);
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:5:1
|
1 | aardvark
2 | beetle
3 | canary
4 | dog
5 | elephant
| ^^^^^^^^
6 | finch
7 | gorilla
8 | hippopotamus
9 | inchworm
10 | jackrabbit
11 | kangaroo
|
",
);
}
#[test]
fn multiple_annotations_non_overlapping() {
let mut env = TestEnvironment::new();
env.add("animals", ANIMALS);
let diag = env
.err()
.primary("animals", "1", "1", "")
.primary("animals", "11", "11", "")
.build();
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:1:1
|
1 | aardvark
| ^^^^^^^^
2 | beetle
3 | canary
|
::: animals:11:1
|
9 | inchworm
10 | jackrabbit
11 | kangaroo
| ^^^^^^^^
",
);
}
#[test]
fn multiple_annotations_adjacent_context() {
let mut env = TestEnvironment::new();
env.add("animals", ANIMALS);
env.context(1);
let diag = env
.err()
.primary("animals", "1", "1", "")
.primary("animals", "3", "3", "")
.build();
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:1:1
|
1 | aardvark
| ^^^^^^^^
2 | beetle
3 | canary
| ^^^^^^
4 | dog
|
",
);
let diag = env
.err()
.primary("animals", "1", "1", "")
.primary("animals", "4", "4", "")
.build();
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:1:1
|
1 | aardvark
| ^^^^^^^^
2 | beetle
3 | canary
4 | dog
| ^^^
5 | elephant
|
",
);
let diag = env
.err()
.primary("animals", "1", "1", "")
.primary("animals", "5", "5", "")
.build();
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:1:1
|
1 | aardvark
| ^^^^^^^^
2 | beetle
|
::: animals:5:1
|
4 | dog
5 | elephant
| ^^^^^^^^
6 | finch
|
",
);
env.context(3);
let diag = env
.err()
.primary("animals", "1", "1", "")
.primary("animals", "5", "5", "")
.build();
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:1:1
|
1 | aardvark
| ^^^^^^^^
2 | beetle
3 | canary
4 | dog
5 | elephant
| ^^^^^^^^
6 | finch
7 | gorilla
8 | hippopotamus
|
",
);
let diag = env
.err()
.primary("animals", "1", "1", "")
.primary("animals", "8", "8", "")
.build();
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:1:1
|
1 | aardvark
| ^^^^^^^^
2 | beetle
3 | canary
4 | dog
5 | elephant
6 | finch
7 | gorilla
8 | hippopotamus
| ^^^^^^^^^^^^
9 | inchworm
10 | jackrabbit
11 | kangaroo
|
",
);
let diag = env
.err()
.primary("animals", "1", "1", "")
.primary("animals", "9", "9", "")
.build();
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:1:1
|
1 | aardvark
| ^^^^^^^^
2 | beetle
3 | canary
4 | dog
|
::: animals:9:1
|
6 | finch
7 | gorilla
8 | hippopotamus
9 | inchworm
| ^^^^^^^^
10 | jackrabbit
11 | kangaroo
|
",
);
}
#[test]
fn trimmed_context() {
let mut env = TestEnvironment::new();
env.add("spacey-animals", SPACEY_ANIMALS);
env.context(2);
let diag = env.err().primary("spacey-animals", "8", "8", "").build();
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> spacey-animals:8:1
|
7 | dog
8 | elephant
| ^^^^^^^^
9 | finch
|
",
);
let diag = env.err().primary("spacey-animals", "12", "12", "").build();
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> spacey-animals:12:1
|
11 | gorilla
12 | hippopotamus
| ^^^^^^^^^^^^
13 | inchworm
14 | jackrabbit
|
",
);
let diag = env.err().primary("spacey-animals", "13", "13", "").build();
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> spacey-animals:13:1
|
11 | gorilla
12 | hippopotamus
13 | inchworm
| ^^^^^^^^
14 | jackrabbit
|
",
);
}
#[test]
fn multiple_annotations_trimmed_context() {
let mut env = TestEnvironment::new();
env.add("spacey-animals", SPACEY_ANIMALS);
env.context(1);
let diag = env
.err()
.primary("spacey-animals", "3", "3", "")
.primary("spacey-animals", "5", "5", "")
.build();
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> spacey-animals:3:1
|
3 | beetle
| ^^^^^^
|
::: spacey-animals:5:1
|
5 | canary
| ^^^^^^
",
);
}
#[test]
fn multiple_files_basic() {
let mut env = TestEnvironment::new();
env.add("animals", ANIMALS);
env.add("fruits", FRUITS);
let diag = env
.err()
.primary("animals", "3", "3", "")
.primary("fruits", "3", "3", "")
.build();
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:3:1
|
1 | aardvark
2 | beetle
3 | canary
| ^^^^^^
4 | dog
5 | elephant
|
::: fruits:3:1
|
1 | apple
2 | banana
3 | cantaloupe
| ^^^^^^^^^^
4 | lime
5 | orange
|
",
);
}
#[test]
fn sub_diag_note_only_message() {
let mut env = TestEnvironment::new();
env.add("animals", ANIMALS);
env.add("fruits", FRUITS);
let mut diag = env.err().primary("animals", "3", "3", "").build();
diag.sub(
env.sub_builder(SubDiagnosticSeverity::Info, "this is a helpful note")
.build(),
);
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:3:1
|
1 | aardvark
2 | beetle
3 | canary
| ^^^^^^
4 | dog
5 | elephant
|
info: this is a helpful note
",
);
}
#[test]
fn sub_diag_many_notes() {
let mut env = TestEnvironment::new();
env.add("animals", ANIMALS);
env.add("fruits", FRUITS);
let mut diag = env.err().primary("animals", "3", "3", "").build();
diag.sub(
env.sub_builder(SubDiagnosticSeverity::Info, "this is a helpful note")
.build(),
);
diag.sub(
env.sub_builder(SubDiagnosticSeverity::Info, "another helpful note")
.build(),
);
diag.sub(
env.sub_builder(SubDiagnosticSeverity::Info, "and another helpful note")
.build(),
);
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:3:1
|
1 | aardvark
2 | beetle
3 | canary
| ^^^^^^
4 | dog
5 | elephant
|
info: this is a helpful note
info: another helpful note
info: and another helpful note
",
);
}
#[test]
fn sub_diag_warning_with_annotation() {
let mut env = TestEnvironment::new();
env.add("animals", ANIMALS);
env.add("fruits", FRUITS);
let mut diag = env.err().primary("animals", "3", "3", "").build();
diag.sub(env.sub_warn().primary("fruits", "3", "3", "").build());
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:3:1
|
1 | aardvark
2 | beetle
3 | canary
| ^^^^^^
4 | dog
5 | elephant
|
warning: sub-diagnostic message
--> fruits:3:1
|
1 | apple
2 | banana
3 | cantaloupe
| ^^^^^^^^^^
4 | lime
5 | orange
|
",
);
}
#[test]
fn sub_diag_many_warning_with_annotation_order() {
let mut env = TestEnvironment::new();
env.add("animals", ANIMALS);
env.add("fruits", FRUITS);
let mut diag = env.err().primary("animals", "3", "3", "").build();
diag.sub(env.sub_warn().primary("fruits", "3", "3", "").build());
diag.sub(env.sub_warn().primary("animals", "11", "11", "").build());
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:3:1
|
1 | aardvark
2 | beetle
3 | canary
| ^^^^^^
4 | dog
5 | elephant
|
warning: sub-diagnostic message
--> fruits:3:1
|
1 | apple
2 | banana
3 | cantaloupe
| ^^^^^^^^^^
4 | lime
5 | orange
|
warning: sub-diagnostic message
--> animals:11:1
|
9 | inchworm
10 | jackrabbit
11 | kangaroo
| ^^^^^^^^
",
);
let mut diag = env.err().primary("animals", "3", "3", "").build();
diag.sub(env.sub_warn().primary("animals", "11", "11", "").build());
diag.sub(env.sub_warn().primary("fruits", "3", "3", "").build());
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:3:1
|
1 | aardvark
2 | beetle
3 | canary
| ^^^^^^
4 | dog
5 | elephant
|
warning: sub-diagnostic message
--> animals:11:1
|
9 | inchworm
10 | jackrabbit
11 | kangaroo
| ^^^^^^^^
warning: sub-diagnostic message
--> fruits:3:1
|
1 | apple
2 | banana
3 | cantaloupe
| ^^^^^^^^^^
4 | lime
5 | orange
|
",
);
}
#[test]
fn sub_diag_repeats_snippet() {
let mut env = TestEnvironment::new();
env.add("animals", ANIMALS);
let mut diag = env.err().primary("animals", "3", "3", "").build();
diag.sub(env.sub_warn().secondary("animals", "3", "3", "").build());
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:3:1
|
1 | aardvark
2 | beetle
3 | canary
| ^^^^^^
4 | dog
5 | elephant
|
warning: sub-diagnostic message
--> animals:3:1
|
1 | aardvark
2 | beetle
3 | canary
| ------
4 | dog
5 | elephant
|
",
);
}
#[test]
fn annotation_multi_line() {
let mut env = TestEnvironment::new();
env.add("animals", ANIMALS);
let diag = env.err().primary("animals", "5", "6", "").build();
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:5:1
|
3 | canary
4 | dog
5 | / elephant
6 | | finch
| |_____^
7 | gorilla
8 | hippopotamus
|
",
);
let diag = env.err().primary("animals", "5", "7:0", "").build();
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:5:1
|
3 | canary
4 | dog
5 | / elephant
6 | | finch
| |_____^
7 | gorilla
8 | hippopotamus
|
",
);
let diag = env.err().primary("animals", "5", "7:1", "").build();
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:5:1
|
3 | canary
4 | dog
5 | / elephant
6 | | finch
7 | | gorilla
| |_^
8 | hippopotamus
9 | inchworm
|
",
);
let diag = env.err().primary("animals", "5:3", "8:8", "").build();
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:5:4
|
3 | canary
4 | dog
5 | elephant
| ____^
6 | | finch
7 | | gorilla
8 | | hippopotamus
| |________^
9 | inchworm
10 | jackrabbit
|
",
);
let diag = env.err().secondary("animals", "5:3", "8:8", "").build();
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:5:4
|
3 | canary
4 | dog
5 | elephant
| ____-
6 | | finch
7 | | gorilla
8 | | hippopotamus
| |________-
9 | inchworm
10 | jackrabbit
|
",
);
}
#[test]
fn annotation_overlapping_multi_line() {
let mut env = TestEnvironment::new();
env.add("animals", ANIMALS);
let diag = env
.err()
.primary("animals", "5", "6", "")
.primary("animals", "4", "7", "")
.build();
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:4:1
|
2 | beetle
3 | canary
4 | / dog
5 | |/ elephant
6 | || finch
| ||_____^
7 | | gorilla
| |________^
8 | hippopotamus
9 | inchworm
|
",
);
let diag = env
.err()
.primary("animals", "4", "7", "")
.primary("animals", "5", "6", "")
.build();
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:4:1
|
2 | beetle
3 | canary
4 | / dog
5 | |/ elephant
6 | || finch
| ||_____^
7 | | gorilla
| |________^
8 | hippopotamus
9 | inchworm
|
",
);
let diag = env
.err()
.primary("animals", "5", "7", "")
.primary("animals", "6", "7", "")
.build();
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:5:1
|
3 | canary
4 | dog
5 | / elephant
6 | |/ finch
7 | || gorilla
| ||_______^
| |_______|
|
8 | hippopotamus
9 | inchworm
|
",
);
let diag = env
.err()
.primary("animals", "5", "6", "")
.primary("animals", "5", "7", "")
.build();
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:5:1
|
3 | canary
4 | dog
5 | // elephant
6 | || finch
| ||_____^
7 | | gorilla
| |________^
8 | hippopotamus
9 | inchworm
|
",
);
let diag = env
.err()
.primary("animals", "5", "6", "")
.primary("animals", "6", "7", "")
.build();
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:5:1
|
3 | canary
4 | dog
5 | / elephant
6 | | finch
| |__^___^
| _|
| |
7 | | gorilla
| |_______^
8 | hippopotamus
9 | inchworm
|
",
);
}
#[test]
fn annotation_message() {
let mut env = TestEnvironment::new();
env.add("animals", ANIMALS);
let diag = env
.err()
.primary("animals", "5:2", "5:6", "giant land mammal")
.build();
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:5:3
|
3 | canary
4 | dog
5 | elephant
| ^^^^ giant land mammal
6 | finch
7 | gorilla
|
",
);
let diag = env
.err()
.primary("animals", "5:2", "5:6", "giant land mammal")
.secondary("animals", "5:2", "5:6", "but afraid of mice")
.build();
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:5:3
|
3 | canary
4 | dog
5 | elephant
| ^^^^
| |
| giant land mammal
| but afraid of mice
6 | finch
7 | gorilla
|
",
);
}
#[test]
fn annotation_one_file_primary_always_comes_first() {
let mut env = TestEnvironment::new();
env.add("animals", ANIMALS);
let diag = env
.err()
.secondary("animals", "1", "1", "secondary")
.primary("animals", "8", "8", "primary")
.build();
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:8:1
|
6 | finch
7 | gorilla
8 | hippopotamus
| ^^^^^^^^^^^^ primary
9 | inchworm
10 | jackrabbit
|
::: animals:1:1
|
1 | aardvark
| -------- secondary
2 | beetle
3 | canary
|
",
);
env.context(0);
let diag = env
.err()
.secondary("animals", "7", "7", "secondary 7")
.primary("animals", "9", "9", "primary 9")
.secondary("animals", "3", "3", "secondary 3")
.secondary("animals", "1", "1", "secondary 1")
.primary("animals", "5", "5", "primary 5")
.build();
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:5:1
|
5 | elephant
| ^^^^^^^^ primary 5
|
::: animals:9:1
|
9 | inchworm
| ^^^^^^^^ primary 9
|
::: animals:1:1
|
1 | aardvark
| -------- secondary 1
|
::: animals:3:1
|
3 | canary
| ------ secondary 3
|
::: animals:7:1
|
7 | gorilla
| ------- secondary 7
",
);
}
#[test]
fn annotation_many_files_primary_always_comes_first() {
let mut env = TestEnvironment::new();
env.add("animals", ANIMALS);
env.add("fruits", FRUITS);
let diag = env
.err()
.secondary("animals", "1", "1", "secondary")
.primary("fruits", "1", "1", "primary")
.build();
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> fruits:1:1
|
1 | apple
| ^^^^^ primary
2 | banana
3 | cantaloupe
|
::: animals:1:1
|
1 | aardvark
| -------- secondary
2 | beetle
3 | canary
|
",
);
env.context(0);
let diag = env
.err()
.secondary("animals", "7", "7", "secondary animals 7")
.secondary("fruits", "2", "2", "secondary fruits 2")
.secondary("animals", "3", "3", "secondary animals 3")
.secondary("animals", "1", "1", "secondary animals 1")
.primary("animals", "11", "11", "primary animals 11")
.primary("fruits", "10", "10", "primary fruits 10")
.build();
insta::assert_snapshot!(
env.render(&diag),
@"
error[test-diagnostic]: main diagnostic message
--> animals:11:1
|
11 | kangaroo
| ^^^^^^^^ primary animals 11
|
::: animals:1:1
|
1 | aardvark
| -------- secondary animals 1
|
::: animals:3:1
|
3 | canary
| ------ secondary animals 3
|
::: animals:7:1
|
7 | gorilla
| ------- secondary animals 7
|
::: fruits:10:1
|
10 | watermelon
| ^^^^^^^^^^ primary fruits 10
|
::: fruits:2:1
|
2 | banana
| ------ secondary fruits 2
",
);
}
#[test]
fn diagnostics_with_equal_locations_sort_by_concise_message() {
let mut env = TestEnvironment::new();
env.add("fruits", FRUITS);
let mut diagnostics = [
env.invalid_syntax("checking mod.py")
.primary("fruits", "1", "1", "")
.build(),
env.invalid_syntax("checking main.py")
.primary("fruits", "1", "1", "")
.build(),
];
diagnostics.sort_by(|left, right| {
left.rendering_sort_key(&env.db)
.cmp(&right.rendering_sort_key(&env.db))
});
assert_eq!(
diagnostics
.iter()
.map(Diagnostic::headline_message)
.collect::<Vec<_>>(),
["checking main.py", "checking mod.py"]
);
}
pub(super) struct TestEnvironment {
db: TestDb,
config: DisplayDiagnosticConfig,
}
impl TestEnvironment {
pub(super) fn new() -> TestEnvironment {
let mut env = TestEnvironment {
db: TestDb::new(),
config: DisplayDiagnosticConfig::new("ty"),
};
env.merge_window(0);
env
}
pub(super) fn context(&mut self, lines: usize) {
let config = self.config.clone();
self.config = config.context(lines);
}
pub(super) fn merge_window(&mut self, lines: usize) {
let config = self.config.clone();
self.config = config.merge_window(lines);
}
pub(super) fn format(&mut self, format: DiagnosticFormat) {
let config = self.config.clone();
self.config = config.format(format);
}
#[allow(
dead_code,
reason = "This is currently only used for JSON but will be needed soon for other formats"
)]
pub(super) fn preview(&mut self, yes: bool) {
let config = self.config.clone();
self.config = config.preview(yes);
}
pub(super) fn hide_severity(&mut self, yes: bool) {
let config = self.config.clone();
self.config = config.hide_severity(yes);
}
pub(super) fn show_fix_status(&mut self, yes: bool) {
let config = self.config.clone();
self.config = config.with_show_fix_status(yes);
}
pub(super) fn fix_applicability(&mut self, applicability: Applicability) {
let config = self.config.clone();
self.config = config.with_fix_applicability(applicability);
}
pub(super) fn add(&mut self, path: &str, contents: &str) {
let path = SystemPath::new(path);
self.db.write_file(path, contents).unwrap();
}
fn span(&self, path: &str, line_offset_start: &str, line_offset_end: &str) -> Span {
let span = self.path(path);
let file = span.expect_ty_file();
let text = source_text(&self.db, file);
let line_index = line_index(&self.db, file);
let source = SourceCode::new(text.as_str(), &line_index);
let (line_start, offset_start) = parse_line_offset(line_offset_start);
let (line_end, offset_end) = parse_line_offset(line_offset_end);
let start = match offset_start {
None => source.line_start(line_start),
Some(offset) => source.line_start(line_start) + offset,
};
let end = match offset_end {
None => source.line_end(line_end) - TextSize::from(1),
Some(offset) => source.line_start(line_end) + offset,
};
span.with_range(TextRange::new(start, end))
}
pub(super) fn path(&self, path: &str) -> Span {
let file = system_path_to_file(&self.db, path).unwrap();
Span::from(file)
}
pub(super) fn err(&mut self) -> DiagnosticBuilder<'_> {
self.builder(
"test-diagnostic",
Severity::Error,
"main diagnostic message",
)
}
fn sub_warn(&mut self) -> SubDiagnosticBuilder<'_> {
self.sub_builder(SubDiagnosticSeverity::Warning, "sub-diagnostic message")
}
pub(super) fn builder(
&mut self,
identifier: &'static str,
severity: Severity,
message: &str,
) -> DiagnosticBuilder<'_> {
let diag = Diagnostic::new(id(identifier), severity, message);
DiagnosticBuilder { env: self, diag }
}
fn invalid_syntax(&mut self, message: &str) -> DiagnosticBuilder<'_> {
let diag = Diagnostic::new(DiagnosticId::InvalidSyntax, Severity::Error, message);
DiagnosticBuilder { env: self, diag }
}
fn sub_builder(
&mut self,
severity: SubDiagnosticSeverity,
message: &str,
) -> SubDiagnosticBuilder<'_> {
let subdiag = SubDiagnostic::new(severity, message);
SubDiagnosticBuilder { env: self, subdiag }
}
pub(super) fn render(&self, diag: &Diagnostic) -> String {
diag.display(&self.db, &self.config).to_string()
}
pub(super) fn render_diagnostics(&self, diagnostics: &[Diagnostic]) -> String {
DisplayDiagnostics::new(&self.db, &self.config, diagnostics).to_string()
}
}
pub(super) struct DiagnosticBuilder<'e> {
env: &'e mut TestEnvironment,
diag: Diagnostic,
}
impl<'e> DiagnosticBuilder<'e> {
pub(super) fn build(self) -> Diagnostic {
self.diag
}
pub(super) fn primary(
mut self,
path: &str,
line_offset_start: &str,
line_offset_end: &str,
label: &str,
) -> DiagnosticBuilder<'e> {
let span = self.env.span(path, line_offset_start, line_offset_end);
let mut ann = Annotation::primary(span);
if !label.is_empty() {
ann = ann.message(label);
}
self.diag.annotate(ann);
self
}
pub(super) fn secondary(
mut self,
path: &str,
line_offset_start: &str,
line_offset_end: &str,
label: &str,
) -> DiagnosticBuilder<'e> {
let span = self.env.span(path, line_offset_start, line_offset_end);
let mut ann = Annotation::secondary(span);
if !label.is_empty() {
ann = ann.message(label);
}
self.diag.annotate(ann);
self
}
fn secondary_code(mut self, secondary_code: &str) -> DiagnosticBuilder<'e> {
self.diag
.set_secondary_code(SecondaryCode::new(secondary_code.to_string()));
self
}
fn fix(mut self, fix: Fix) -> DiagnosticBuilder<'e> {
self.diag.set_fix(fix);
self
}
fn noqa_offset(mut self, noqa_offset: TextSize) -> DiagnosticBuilder<'e> {
self.diag.set_noqa_offset(noqa_offset);
self
}
pub(super) fn help(mut self, message: impl IntoDiagnosticMessage) -> DiagnosticBuilder<'e> {
self.diag.help(message);
self
}
fn sub(
mut self,
f: impl Fn(&mut TestEnvironment) -> SubDiagnostic,
) -> DiagnosticBuilder<'e> {
let sub = f(self.env);
self.diag.sub(sub);
self
}
pub(super) fn documentation_url(mut self, url: impl Into<String>) -> DiagnosticBuilder<'e> {
self.diag.set_documentation_url(Some(url.into()));
self
}
}
struct SubDiagnosticBuilder<'e> {
env: &'e mut TestEnvironment,
subdiag: SubDiagnostic,
}
impl<'e> SubDiagnosticBuilder<'e> {
fn build(self) -> SubDiagnostic {
self.subdiag
}
fn primary(
mut self,
path: &str,
line_offset_start: &str,
line_offset_end: &str,
label: &str,
) -> SubDiagnosticBuilder<'e> {
let span = self.env.span(path, line_offset_start, line_offset_end);
let mut ann = Annotation::primary(span);
if !label.is_empty() {
ann = ann.message(label);
}
self.subdiag.annotate(ann);
self
}
fn secondary(
mut self,
path: &str,
line_offset_start: &str,
line_offset_end: &str,
label: &str,
) -> SubDiagnosticBuilder<'e> {
let span = self.env.span(path, line_offset_start, line_offset_end);
let mut ann = Annotation::secondary(span);
if !label.is_empty() {
ann = ann.message(label);
}
self.subdiag.annotate(ann);
self
}
}
fn id(lint_name: &'static str) -> DiagnosticId {
DiagnosticId::lint(lint_name)
}
fn parse_line_offset(s: &str) -> (OneIndexed, Option<TextSize>) {
let Some((line, offset)) = s.split_once(":") else {
let line_number = OneIndexed::new(s.parse().unwrap()).unwrap();
return (line_number, None);
};
let line_number = OneIndexed::new(line.parse().unwrap()).unwrap();
let offset = TextSize::from(offset.parse::<u32>().unwrap());
(line_number, Some(offset))
}
pub(crate) fn create_diagnostics(
format: DiagnosticFormat,
) -> (TestEnvironment, Vec<Diagnostic>) {
let mut env = TestEnvironment::new();
env.add(
"fib.py",
r#"import os
def fibonacci(n):
"""Compute the nth number in the Fibonacci sequence."""
x = 1
if n == 0:
return 0
elif n == 1:
return 1
else:
return fibonaccii(n - 1) + fibonacci(n - 2)
"#,
);
env.add("undef.py", r"if a == 1: pass");
env.format(format);
let diagnostics = vec![
env.builder("unused-import", Severity::Error, "`os` imported but unused")
.primary("fib.py", "1:7", "1:9", "")
.help("Remove unused import: `os`")
.secondary_code("F401")
.fix(Fix::unsafe_edit(Edit::range_deletion(TextRange::new(
TextSize::from(0),
TextSize::from(10),
))))
.noqa_offset(TextSize::from(7))
.documentation_url("https://docs.astral.sh/ruff/rules/unused-import")
.build(),
env.builder(
"unused-variable",
Severity::Error,
"Local variable `x` is assigned to but never used",
)
.primary("fib.py", "6:4", "6:5", "")
.help("Remove assignment to unused variable `x`")
.secondary_code("F841")
.fix(Fix::unsafe_edit(Edit::deletion(
TextSize::from(94),
TextSize::from(99),
)))
.noqa_offset(TextSize::from(94))
.documentation_url("https://docs.astral.sh/ruff/rules/unused-variable")
.build(),
env.builder("undefined-name", Severity::Error, "Undefined name `a`")
.primary("undef.py", "1:3", "1:4", "")
.secondary_code("F821")
.noqa_offset(TextSize::from(3))
.documentation_url("https://docs.astral.sh/ruff/rules/undefined-name")
.build(),
env.builder(
"undefined-name",
Severity::Error,
"Undefined name `fibonaccii`",
)
.primary("fib.py", "12:15", "12:25", "")
.secondary_code("F821")
.noqa_offset(ruff_text_size::TextSize::from(0))
.documentation_url("https://docs.astral.sh/ruff/rules/undefined-name")
.secondary("fib.py", "12:35", "12:36", "")
.sub(|env| {
env.sub_builder(
SubDiagnosticSeverity::Info,
"Did you mean to import it from `/some/path/def.py`?",
)
.primary("fib.py", "4:4", "4:13", "`fibonacci` is defined here")
.secondary("fib.py", "5:4", "5", "`fibonacci` is documented here")
.build()
})
.build(),
];
(env, diagnostics)
}
pub(crate) fn create_syntax_error_diagnostics(
format: DiagnosticFormat,
) -> (TestEnvironment, Vec<Diagnostic>) {
let mut env = TestEnvironment::new();
env.add(
"syntax_errors.py",
r"from os import
if call(foo
def bar():
pass
",
);
env.format(format);
let diagnostics = vec![
env.invalid_syntax("Expected one or more symbol names after import")
.primary("syntax_errors.py", "1:14", "1:15", "")
.build(),
env.invalid_syntax("Expected ')', found newline")
.primary("syntax_errors.py", "3:11", "3:12", "")
.build(),
];
(env, diagnostics)
}
pub(super) static NOTEBOOK: &str = r##"
{
"cells": [
{
"cell_type": "code",
"metadata": {},
"outputs": [],
"source": [
"# cell 1\n",
"import os"
]
},
{
"cell_type": "code",
"metadata": {},
"outputs": [],
"source": [
"# cell 2\n",
"import math\n",
"\n",
"print('hello world')"
]
},
{
"cell_type": "code",
"metadata": {},
"outputs": [],
"source": [
"# cell 3\n",
"def foo():\n",
" print()\n",
" x = 1\n"
]
}
],
"metadata": {},
"nbformat": 4,
"nbformat_minor": 5
}
"##;
pub(crate) fn create_notebook_diagnostics(
format: DiagnosticFormat,
) -> (TestEnvironment, Vec<Diagnostic>) {
let mut env = TestEnvironment::new();
env.add("notebook.ipynb", NOTEBOOK);
env.format(format);
let diagnostics = vec![
env.builder("unused-import", Severity::Error, "`os` imported but unused")
.primary("notebook.ipynb", "2:7", "2:9", "")
.help("Remove unused import: `os`")
.secondary_code("F401")
.fix(Fix::safe_edit(Edit::range_deletion(TextRange::new(
TextSize::from(9),
TextSize::from(19),
))))
.noqa_offset(TextSize::from(16))
.documentation_url("https://docs.astral.sh/ruff/rules/unused-import")
.build(),
env.builder(
"unused-import",
Severity::Error,
"`math` imported but unused",
)
.primary("notebook.ipynb", "4:7", "4:11", "")
.help("Remove unused import: `math`")
.secondary_code("F401")
.fix(Fix::safe_edit(Edit::range_deletion(TextRange::new(
TextSize::from(28),
TextSize::from(40),
))))
.noqa_offset(TextSize::from(35))
.documentation_url("https://docs.astral.sh/ruff/rules/unused-import")
.build(),
env.builder(
"unused-variable",
Severity::Error,
"Local variable `x` is assigned to but never used",
)
.primary("notebook.ipynb", "10:4", "10:5", "")
.help("Remove assignment to unused variable `x`")
.secondary_code("F841")
.fix(Fix::unsafe_edit(Edit::range_deletion(TextRange::new(
TextSize::from(94),
TextSize::from(104),
))))
.noqa_offset(TextSize::from(98))
.documentation_url("https://docs.astral.sh/ruff/rules/unused-variable")
.build(),
];
(env, diagnostics)
}
}