mirror of https://github.com/astral-sh/ruff
[ty] Add hyperlinks to rule codes in CLI (#21502)
This commit is contained in:
parent
5ca9c15fc8
commit
7043d51df0
|
|
@ -31,7 +31,7 @@
|
||||||
//! styling.
|
//! styling.
|
||||||
//!
|
//!
|
||||||
//! The above snippet has been built out of the following structure:
|
//! The above snippet has been built out of the following structure:
|
||||||
use crate::snippet;
|
use crate::{Id, snippet};
|
||||||
use std::cmp::{Reverse, max, min};
|
use std::cmp::{Reverse, max, min};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
@ -189,6 +189,7 @@ impl DisplaySet<'_> {
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_annotation(
|
fn format_annotation(
|
||||||
&self,
|
&self,
|
||||||
line_offset: usize,
|
line_offset: usize,
|
||||||
|
|
@ -199,11 +200,13 @@ impl DisplaySet<'_> {
|
||||||
) -> fmt::Result {
|
) -> fmt::Result {
|
||||||
let hide_severity = annotation.annotation_type.is_none();
|
let hide_severity = annotation.annotation_type.is_none();
|
||||||
let color = get_annotation_style(&annotation.annotation_type, stylesheet);
|
let color = get_annotation_style(&annotation.annotation_type, stylesheet);
|
||||||
|
|
||||||
let formatted_len = if let Some(id) = &annotation.id {
|
let formatted_len = if let Some(id) = &annotation.id {
|
||||||
|
let id_len = id.id.len();
|
||||||
if hide_severity {
|
if hide_severity {
|
||||||
id.len()
|
id_len
|
||||||
} else {
|
} else {
|
||||||
2 + id.len() + annotation_type_len(&annotation.annotation_type)
|
2 + id_len + annotation_type_len(&annotation.annotation_type)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
annotation_type_len(&annotation.annotation_type)
|
annotation_type_len(&annotation.annotation_type)
|
||||||
|
|
@ -256,9 +259,20 @@ impl DisplaySet<'_> {
|
||||||
let annotation_type = annotation_type_str(&annotation.annotation_type);
|
let annotation_type = annotation_type_str(&annotation.annotation_type);
|
||||||
if let Some(id) = annotation.id {
|
if let Some(id) = annotation.id {
|
||||||
if hide_severity {
|
if hide_severity {
|
||||||
buffer.append(line_offset, &format!("{id} "), *stylesheet.error());
|
buffer.append(
|
||||||
|
line_offset,
|
||||||
|
&format!("{id} ", id = fmt_with_hyperlink(id.id, id.url, stylesheet)),
|
||||||
|
*stylesheet.error(),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
buffer.append(line_offset, &format!("{annotation_type}[{id}]"), *color);
|
buffer.append(
|
||||||
|
line_offset,
|
||||||
|
&format!(
|
||||||
|
"{annotation_type}[{id}]",
|
||||||
|
id = fmt_with_hyperlink(id.id, id.url, stylesheet)
|
||||||
|
),
|
||||||
|
*color,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
buffer.append(line_offset, annotation_type, *color);
|
buffer.append(line_offset, annotation_type, *color);
|
||||||
|
|
@ -707,7 +721,7 @@ impl DisplaySet<'_> {
|
||||||
let style =
|
let style =
|
||||||
get_annotation_style(&annotation.annotation_type, stylesheet);
|
get_annotation_style(&annotation.annotation_type, stylesheet);
|
||||||
let mut formatted_len = if let Some(id) = &annotation.annotation.id {
|
let mut formatted_len = if let Some(id) = &annotation.annotation.id {
|
||||||
2 + id.len()
|
2 + id.id.len()
|
||||||
+ annotation_type_len(&annotation.annotation.annotation_type)
|
+ annotation_type_len(&annotation.annotation.annotation_type)
|
||||||
} else {
|
} else {
|
||||||
annotation_type_len(&annotation.annotation.annotation_type)
|
annotation_type_len(&annotation.annotation.annotation_type)
|
||||||
|
|
@ -724,7 +738,10 @@ impl DisplaySet<'_> {
|
||||||
} else if formatted_len != 0 {
|
} else if formatted_len != 0 {
|
||||||
formatted_len += 2;
|
formatted_len += 2;
|
||||||
let id = match &annotation.annotation.id {
|
let id = match &annotation.annotation.id {
|
||||||
Some(id) => format!("[{id}]"),
|
Some(id) => format!(
|
||||||
|
"[{id}]",
|
||||||
|
id = fmt_with_hyperlink(&id.id, id.url, stylesheet)
|
||||||
|
),
|
||||||
None => String::new(),
|
None => String::new(),
|
||||||
};
|
};
|
||||||
buffer.puts(
|
buffer.puts(
|
||||||
|
|
@ -827,7 +844,7 @@ impl DisplaySet<'_> {
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub(crate) struct Annotation<'a> {
|
pub(crate) struct Annotation<'a> {
|
||||||
pub(crate) annotation_type: DisplayAnnotationType,
|
pub(crate) annotation_type: DisplayAnnotationType,
|
||||||
pub(crate) id: Option<&'a str>,
|
pub(crate) id: Option<Id<'a>>,
|
||||||
pub(crate) label: Vec<DisplayTextFragment<'a>>,
|
pub(crate) label: Vec<DisplayTextFragment<'a>>,
|
||||||
pub(crate) is_fixable: bool,
|
pub(crate) is_fixable: bool,
|
||||||
}
|
}
|
||||||
|
|
@ -1140,7 +1157,7 @@ fn format_message<'m>(
|
||||||
|
|
||||||
fn format_title<'a>(
|
fn format_title<'a>(
|
||||||
level: crate::Level,
|
level: crate::Level,
|
||||||
id: Option<&'a str>,
|
id: Option<Id<'a>>,
|
||||||
label: &'a str,
|
label: &'a str,
|
||||||
is_fixable: bool,
|
is_fixable: bool,
|
||||||
) -> DisplayLine<'a> {
|
) -> DisplayLine<'a> {
|
||||||
|
|
@ -1158,7 +1175,7 @@ fn format_title<'a>(
|
||||||
|
|
||||||
fn format_footer<'a>(
|
fn format_footer<'a>(
|
||||||
level: crate::Level,
|
level: crate::Level,
|
||||||
id: Option<&'a str>,
|
id: Option<Id<'a>>,
|
||||||
label: &'a str,
|
label: &'a str,
|
||||||
) -> Vec<DisplayLine<'a>> {
|
) -> Vec<DisplayLine<'a>> {
|
||||||
let mut result = vec![];
|
let mut result = vec![];
|
||||||
|
|
@ -1706,6 +1723,7 @@ fn format_body<'m>(
|
||||||
annotation: Annotation {
|
annotation: Annotation {
|
||||||
annotation_type,
|
annotation_type,
|
||||||
id: None,
|
id: None,
|
||||||
|
|
||||||
label: format_label(annotation.label, None),
|
label: format_label(annotation.label, None),
|
||||||
is_fixable: false,
|
is_fixable: false,
|
||||||
},
|
},
|
||||||
|
|
@ -1887,3 +1905,40 @@ fn char_width(c: char) -> Option<usize> {
|
||||||
unicode_width::UnicodeWidthChar::width(c)
|
unicode_width::UnicodeWidthChar::width(c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn fmt_with_hyperlink<'a, T>(
|
||||||
|
content: T,
|
||||||
|
url: Option<&'a str>,
|
||||||
|
stylesheet: &Stylesheet,
|
||||||
|
) -> impl std::fmt::Display + 'a
|
||||||
|
where
|
||||||
|
T: std::fmt::Display + 'a,
|
||||||
|
{
|
||||||
|
struct FmtHyperlink<'a, T> {
|
||||||
|
content: T,
|
||||||
|
url: Option<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> std::fmt::Display for FmtHyperlink<'_, T>
|
||||||
|
where
|
||||||
|
T: std::fmt::Display,
|
||||||
|
{
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if let Some(url) = self.url {
|
||||||
|
write!(f, "\x1B]8;;{url}\x1B\\")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.content.fmt(f)?;
|
||||||
|
|
||||||
|
if self.url.is_some() {
|
||||||
|
f.write_str("\x1B]8;;\x1B\\")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = if stylesheet.hyperlink { url } else { None };
|
||||||
|
|
||||||
|
FmtHyperlink { content, url }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ impl Renderer {
|
||||||
}
|
}
|
||||||
.effects(Effects::BOLD),
|
.effects(Effects::BOLD),
|
||||||
none: Style::new(),
|
none: Style::new(),
|
||||||
|
hyperlink: true,
|
||||||
},
|
},
|
||||||
..Self::plain()
|
..Self::plain()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ pub(crate) struct Stylesheet {
|
||||||
pub(crate) line_no: Style,
|
pub(crate) line_no: Style,
|
||||||
pub(crate) emphasis: Style,
|
pub(crate) emphasis: Style,
|
||||||
pub(crate) none: Style,
|
pub(crate) none: Style,
|
||||||
|
pub(crate) hyperlink: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Stylesheet {
|
impl Default for Stylesheet {
|
||||||
|
|
@ -29,6 +30,7 @@ impl Stylesheet {
|
||||||
line_no: Style::new(),
|
line_no: Style::new(),
|
||||||
emphasis: Style::new(),
|
emphasis: Style::new(),
|
||||||
none: Style::new(),
|
none: Style::new(),
|
||||||
|
hyperlink: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,19 @@
|
||||||
|
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Default, PartialEq)]
|
||||||
|
pub(crate) struct Id<'a> {
|
||||||
|
pub(crate) id: &'a str,
|
||||||
|
pub(crate) url: Option<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Primary structure provided for formatting
|
/// Primary structure provided for formatting
|
||||||
///
|
///
|
||||||
/// See [`Level::title`] to create a [`Message`]
|
/// See [`Level::title`] to create a [`Message`]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Message<'a> {
|
pub struct Message<'a> {
|
||||||
pub(crate) level: Level,
|
pub(crate) level: Level,
|
||||||
pub(crate) id: Option<&'a str>,
|
pub(crate) id: Option<Id<'a>>,
|
||||||
pub(crate) title: &'a str,
|
pub(crate) title: &'a str,
|
||||||
pub(crate) snippets: Vec<Snippet<'a>>,
|
pub(crate) snippets: Vec<Snippet<'a>>,
|
||||||
pub(crate) footer: Vec<Message<'a>>,
|
pub(crate) footer: Vec<Message<'a>>,
|
||||||
|
|
@ -28,7 +34,12 @@ pub struct Message<'a> {
|
||||||
|
|
||||||
impl<'a> Message<'a> {
|
impl<'a> Message<'a> {
|
||||||
pub fn id(mut self, id: &'a str) -> Self {
|
pub fn id(mut self, id: &'a str) -> Self {
|
||||||
self.id = Some(id);
|
self.id = Some(Id { id, url: None });
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id_with_url(mut self, id: &'a str, url: Option<&'a str>) -> Self {
|
||||||
|
self.id = Some(Id { id, url });
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ impl Diagnostic {
|
||||||
severity,
|
severity,
|
||||||
message: message.into_diagnostic_message(),
|
message: message.into_diagnostic_message(),
|
||||||
custom_concise_message: None,
|
custom_concise_message: None,
|
||||||
|
documentation_url: None,
|
||||||
annotations: vec![],
|
annotations: vec![],
|
||||||
subs: vec![],
|
subs: vec![],
|
||||||
fix: None,
|
fix: None,
|
||||||
|
|
@ -370,6 +371,14 @@ impl Diagnostic {
|
||||||
.is_some_and(|fix| fix.applies(config.fix_applicability))
|
.is_some_and(|fix| fix.applies(config.fix_applicability))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn documentation_url(&self) -> Option<&str> {
|
||||||
|
self.inner.documentation_url.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_documentation_url(&mut self, url: Option<String>) {
|
||||||
|
Arc::make_mut(&mut self.inner).documentation_url = url;
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the offset of the parent statement for this diagnostic if it exists.
|
/// Returns the offset of the parent statement for this diagnostic if it exists.
|
||||||
///
|
///
|
||||||
/// This is primarily used for checking noqa/secondary code suppressions.
|
/// This is primarily used for checking noqa/secondary code suppressions.
|
||||||
|
|
@ -544,6 +553,7 @@ impl Diagnostic {
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
|
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
|
||||||
struct DiagnosticInner {
|
struct DiagnosticInner {
|
||||||
id: DiagnosticId,
|
id: DiagnosticId,
|
||||||
|
documentation_url: Option<String>,
|
||||||
severity: Severity,
|
severity: Severity,
|
||||||
message: DiagnosticMessage,
|
message: DiagnosticMessage,
|
||||||
custom_concise_message: Option<DiagnosticMessage>,
|
custom_concise_message: Option<DiagnosticMessage>,
|
||||||
|
|
|
||||||
|
|
@ -205,6 +205,7 @@ impl<'a> Resolved<'a> {
|
||||||
struct ResolvedDiagnostic<'a> {
|
struct ResolvedDiagnostic<'a> {
|
||||||
level: AnnotateLevel,
|
level: AnnotateLevel,
|
||||||
id: Option<String>,
|
id: Option<String>,
|
||||||
|
documentation_url: Option<String>,
|
||||||
message: String,
|
message: String,
|
||||||
annotations: Vec<ResolvedAnnotation<'a>>,
|
annotations: Vec<ResolvedAnnotation<'a>>,
|
||||||
is_fixable: bool,
|
is_fixable: bool,
|
||||||
|
|
@ -240,12 +241,12 @@ impl<'a> ResolvedDiagnostic<'a> {
|
||||||
// `DisplaySet::format_annotation` for both cases, but this is a small hack to improve
|
// `DisplaySet::format_annotation` for both cases, but this is a small hack to improve
|
||||||
// the formatting of syntax errors for now. This should also be kept consistent with the
|
// the formatting of syntax errors for now. This should also be kept consistent with the
|
||||||
// concise formatting.
|
// concise formatting.
|
||||||
Some(diag.secondary_code().map_or_else(
|
diag.secondary_code().map_or_else(
|
||||||
|| format!("{id}:", id = diag.inner.id),
|
|| format!("{id}:", id = diag.inner.id),
|
||||||
|code| code.to_string(),
|
|code| code.to_string(),
|
||||||
))
|
)
|
||||||
} else {
|
} else {
|
||||||
Some(diag.inner.id.to_string())
|
diag.inner.id.to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
let level = if config.hide_severity {
|
let level = if config.hide_severity {
|
||||||
|
|
@ -256,7 +257,8 @@ impl<'a> ResolvedDiagnostic<'a> {
|
||||||
|
|
||||||
ResolvedDiagnostic {
|
ResolvedDiagnostic {
|
||||||
level,
|
level,
|
||||||
id,
|
id: Some(id),
|
||||||
|
documentation_url: diag.documentation_url().map(ToString::to_string),
|
||||||
message: diag.inner.message.as_str().to_string(),
|
message: diag.inner.message.as_str().to_string(),
|
||||||
annotations,
|
annotations,
|
||||||
is_fixable: config.show_fix_status && diag.has_applicable_fix(config),
|
is_fixable: config.show_fix_status && diag.has_applicable_fix(config),
|
||||||
|
|
@ -287,6 +289,7 @@ impl<'a> ResolvedDiagnostic<'a> {
|
||||||
ResolvedDiagnostic {
|
ResolvedDiagnostic {
|
||||||
level: diag.inner.severity.to_annotate(),
|
level: diag.inner.severity.to_annotate(),
|
||||||
id: None,
|
id: None,
|
||||||
|
documentation_url: None,
|
||||||
message: diag.inner.message.as_str().to_string(),
|
message: diag.inner.message.as_str().to_string(),
|
||||||
annotations,
|
annotations,
|
||||||
is_fixable: false,
|
is_fixable: false,
|
||||||
|
|
@ -385,6 +388,7 @@ impl<'a> ResolvedDiagnostic<'a> {
|
||||||
RenderableDiagnostic {
|
RenderableDiagnostic {
|
||||||
level: self.level,
|
level: self.level,
|
||||||
id: self.id.as_deref(),
|
id: self.id.as_deref(),
|
||||||
|
documentation_url: self.documentation_url.as_deref(),
|
||||||
message: &self.message,
|
message: &self.message,
|
||||||
snippets_by_input,
|
snippets_by_input,
|
||||||
is_fixable: self.is_fixable,
|
is_fixable: self.is_fixable,
|
||||||
|
|
@ -485,6 +489,7 @@ struct RenderableDiagnostic<'r> {
|
||||||
/// An ID is always present for top-level diagnostics and always absent for
|
/// An ID is always present for top-level diagnostics and always absent for
|
||||||
/// sub-diagnostics.
|
/// sub-diagnostics.
|
||||||
id: Option<&'r str>,
|
id: Option<&'r str>,
|
||||||
|
documentation_url: Option<&'r str>,
|
||||||
/// The message emitted with the diagnostic, before any snippets are
|
/// The message emitted with the diagnostic, before any snippets are
|
||||||
/// rendered.
|
/// rendered.
|
||||||
message: &'r str,
|
message: &'r str,
|
||||||
|
|
@ -519,7 +524,7 @@ impl RenderableDiagnostic<'_> {
|
||||||
.is_fixable(self.is_fixable)
|
.is_fixable(self.is_fixable)
|
||||||
.lineno_offset(self.header_offset);
|
.lineno_offset(self.header_offset);
|
||||||
if let Some(id) = self.id {
|
if let Some(id) = self.id {
|
||||||
message = message.id(id);
|
message = message.id_with_url(id, self.documentation_url);
|
||||||
}
|
}
|
||||||
message.snippets(snippets)
|
message.snippets(snippets)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::diagnostic::{
|
use crate::diagnostic::{
|
||||||
Diagnostic, DisplayDiagnosticConfig, Severity,
|
Diagnostic, DisplayDiagnosticConfig, Severity,
|
||||||
stylesheet::{DiagnosticStylesheet, fmt_styled},
|
stylesheet::{DiagnosticStylesheet, fmt_styled, fmt_with_hyperlink},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::FileResolver;
|
use super::FileResolver;
|
||||||
|
|
@ -62,18 +62,29 @@ impl<'a> ConciseRenderer<'a> {
|
||||||
}
|
}
|
||||||
write!(f, "{sep} ")?;
|
write!(f, "{sep} ")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.config.hide_severity {
|
if self.config.hide_severity {
|
||||||
if let Some(code) = diag.secondary_code() {
|
if let Some(code) = diag.secondary_code() {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"{code} ",
|
"{code} ",
|
||||||
code = fmt_styled(code, stylesheet.secondary_code)
|
code = fmt_styled(
|
||||||
|
fmt_with_hyperlink(&code, diag.documentation_url(), &stylesheet),
|
||||||
|
stylesheet.secondary_code
|
||||||
|
)
|
||||||
)?;
|
)?;
|
||||||
} else {
|
} else {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"{id}: ",
|
"{id}: ",
|
||||||
id = fmt_styled(diag.inner.id.as_str(), stylesheet.secondary_code)
|
id = fmt_styled(
|
||||||
|
fmt_with_hyperlink(
|
||||||
|
&diag.inner.id,
|
||||||
|
diag.documentation_url(),
|
||||||
|
&stylesheet
|
||||||
|
),
|
||||||
|
stylesheet.secondary_code
|
||||||
|
)
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
if self.config.show_fix_status {
|
if self.config.show_fix_status {
|
||||||
|
|
@ -93,7 +104,10 @@ impl<'a> ConciseRenderer<'a> {
|
||||||
f,
|
f,
|
||||||
"{severity}[{id}] ",
|
"{severity}[{id}] ",
|
||||||
severity = fmt_styled(severity, severity_style),
|
severity = fmt_styled(severity, severity_style),
|
||||||
id = fmt_styled(diag.id(), stylesheet.emphasis)
|
id = fmt_styled(
|
||||||
|
fmt_with_hyperlink(&diag.id(), diag.documentation_url(), &stylesheet),
|
||||||
|
stylesheet.emphasis
|
||||||
|
)
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,43 @@ where
|
||||||
FmtStyled { content, style }
|
FmtStyled { content, style }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn fmt_with_hyperlink<'a, T>(
|
||||||
|
content: T,
|
||||||
|
url: Option<&'a str>,
|
||||||
|
stylesheet: &DiagnosticStylesheet,
|
||||||
|
) -> impl std::fmt::Display + 'a
|
||||||
|
where
|
||||||
|
T: std::fmt::Display + 'a,
|
||||||
|
{
|
||||||
|
struct FmtHyperlink<'a, T> {
|
||||||
|
content: T,
|
||||||
|
url: Option<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> std::fmt::Display for FmtHyperlink<'_, T>
|
||||||
|
where
|
||||||
|
T: std::fmt::Display,
|
||||||
|
{
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if let Some(url) = self.url {
|
||||||
|
write!(f, "\x1B]8;;{url}\x1B\\")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.content.fmt(f)?;
|
||||||
|
|
||||||
|
if self.url.is_some() {
|
||||||
|
f.write_str("\x1B]8;;\x1B\\")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = if stylesheet.hyperlink { url } else { None };
|
||||||
|
|
||||||
|
FmtHyperlink { content, url }
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct DiagnosticStylesheet {
|
pub struct DiagnosticStylesheet {
|
||||||
pub(crate) error: Style,
|
pub(crate) error: Style,
|
||||||
|
|
@ -47,6 +84,7 @@ pub struct DiagnosticStylesheet {
|
||||||
pub(crate) deletion: Style,
|
pub(crate) deletion: Style,
|
||||||
pub(crate) insertion_line_no: Style,
|
pub(crate) insertion_line_no: Style,
|
||||||
pub(crate) deletion_line_no: Style,
|
pub(crate) deletion_line_no: Style,
|
||||||
|
pub(crate) hyperlink: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for DiagnosticStylesheet {
|
impl Default for DiagnosticStylesheet {
|
||||||
|
|
@ -74,6 +112,7 @@ impl DiagnosticStylesheet {
|
||||||
deletion: AnsiColor::Red.on_default(),
|
deletion: AnsiColor::Red.on_default(),
|
||||||
insertion_line_no: AnsiColor::Green.on_default().effects(Effects::BOLD),
|
insertion_line_no: AnsiColor::Green.on_default().effects(Effects::BOLD),
|
||||||
deletion_line_no: AnsiColor::Red.on_default().effects(Effects::BOLD),
|
deletion_line_no: AnsiColor::Red.on_default().effects(Effects::BOLD),
|
||||||
|
hyperlink: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,6 +132,7 @@ impl DiagnosticStylesheet {
|
||||||
deletion: Style::new(),
|
deletion: Style::new(),
|
||||||
insertion_line_no: Style::new(),
|
insertion_line_no: Style::new(),
|
||||||
deletion_line_no: Style::new(),
|
deletion_line_no: Style::new(),
|
||||||
|
hyperlink: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
|
use crate::{Db, Program, PythonVersionWithSource};
|
||||||
|
use ruff_db::diagnostic::{Annotation, Diagnostic, SubDiagnostic, SubDiagnosticSeverity};
|
||||||
|
use std::fmt::Write;
|
||||||
|
|
||||||
/// Suggest a name from `existing_names` that is similar to `wrong_name`.
|
/// Suggest a name from `existing_names` that is similar to `wrong_name`.
|
||||||
pub(crate) fn did_you_mean<S: AsRef<str>, T: AsRef<str>>(
|
pub(crate) fn did_you_mean<S: AsRef<str>, T: AsRef<str>>(
|
||||||
existing_names: impl Iterator<Item = S>,
|
existing_names: impl Iterator<Item = S>,
|
||||||
|
|
@ -24,10 +28,6 @@ pub(crate) fn did_you_mean<S: AsRef<str>, T: AsRef<str>>(
|
||||||
.map(|(id, _)| id)
|
.map(|(id, _)| id)
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::{Db, Program, PythonVersionWithSource};
|
|
||||||
use ruff_db::diagnostic::{Annotation, Diagnostic, SubDiagnostic, SubDiagnosticSeverity};
|
|
||||||
use std::fmt::Write;
|
|
||||||
|
|
||||||
/// Add a subdiagnostic to `diagnostic` that explains why a certain Python version was inferred.
|
/// Add a subdiagnostic to `diagnostic` that explains why a certain Python version was inferred.
|
||||||
///
|
///
|
||||||
/// ty can infer the Python version from various sources, such as command-line arguments,
|
/// ty can infer the Python version from various sources, such as command-line arguments,
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,10 @@ impl LintMetadata {
|
||||||
self.documentation_lines().join("\n")
|
self.documentation_lines().join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn documentation_url(&self) -> String {
|
||||||
|
lint_documentation_url(self.name())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn default_level(&self) -> Level {
|
pub fn default_level(&self) -> Level {
|
||||||
self.default_level
|
self.default_level
|
||||||
}
|
}
|
||||||
|
|
@ -133,6 +137,10 @@ impl LintMetadata {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn lint_documentation_url(lint_name: LintName) -> String {
|
||||||
|
format!("https://ty.dev/rules#{lint_name}")
|
||||||
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub const fn lint_metadata_defaults() -> LintMetadata {
|
pub const fn lint_metadata_defaults() -> LintMetadata {
|
||||||
LintMetadata {
|
LintMetadata {
|
||||||
|
|
|
||||||
|
|
@ -298,6 +298,7 @@ impl<'a> CheckSuppressionsContext<'a> {
|
||||||
|
|
||||||
let id = DiagnosticId::Lint(lint.name());
|
let id = DiagnosticId::Lint(lint.name());
|
||||||
let mut diag = Diagnostic::new(id, severity, "");
|
let mut diag = Diagnostic::new(id, severity, "");
|
||||||
|
diag.set_documentation_url(Some(lint.documentation_url()));
|
||||||
let span = Span::from(self.file).with_range(range);
|
let span = Span::from(self.file).with_range(range);
|
||||||
diag.annotate(Annotation::primary(span).message(message));
|
diag.annotate(Annotation::primary(span).message(message));
|
||||||
self.diagnostics.push(diag);
|
self.diagnostics.push(diag);
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ use ruff_text_size::{Ranged, TextRange};
|
||||||
|
|
||||||
use super::{Type, TypeCheckDiagnostics, binding_type};
|
use super::{Type, TypeCheckDiagnostics, binding_type};
|
||||||
|
|
||||||
use crate::lint::LintSource;
|
use crate::lint::{LintSource, lint_documentation_url};
|
||||||
use crate::semantic_index::scope::ScopeId;
|
use crate::semantic_index::scope::ScopeId;
|
||||||
use crate::semantic_index::semantic_index;
|
use crate::semantic_index::semantic_index;
|
||||||
use crate::types::function::FunctionDecorators;
|
use crate::types::function::FunctionDecorators;
|
||||||
|
|
@ -103,7 +103,7 @@ impl<'db, 'ast> InferContext<'db, 'ast> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn is_lint_enabled(&self, lint: &'static LintMetadata) -> bool {
|
pub(super) fn is_lint_enabled(&self, lint: &'static LintMetadata) -> bool {
|
||||||
LintDiagnosticGuardBuilder::severity_and_source(self, lint).is_some()
|
LintDiagnosticGuardBuilder::severity_and_source(self, LintId::of(lint)).is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Optionally return a builder for a lint diagnostic guard.
|
/// Optionally return a builder for a lint diagnostic guard.
|
||||||
|
|
@ -395,7 +395,7 @@ impl Drop for LintDiagnosticGuard<'_, '_> {
|
||||||
/// when the diagnostic is disabled or suppressed (among other reasons).
|
/// when the diagnostic is disabled or suppressed (among other reasons).
|
||||||
pub(super) struct LintDiagnosticGuardBuilder<'db, 'ctx> {
|
pub(super) struct LintDiagnosticGuardBuilder<'db, 'ctx> {
|
||||||
ctx: &'ctx InferContext<'db, 'ctx>,
|
ctx: &'ctx InferContext<'db, 'ctx>,
|
||||||
id: DiagnosticId,
|
id: LintId,
|
||||||
severity: Severity,
|
severity: Severity,
|
||||||
source: LintSource,
|
source: LintSource,
|
||||||
primary_span: Span,
|
primary_span: Span,
|
||||||
|
|
@ -404,7 +404,7 @@ pub(super) struct LintDiagnosticGuardBuilder<'db, 'ctx> {
|
||||||
impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> {
|
impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> {
|
||||||
fn severity_and_source(
|
fn severity_and_source(
|
||||||
ctx: &'ctx InferContext<'db, 'ctx>,
|
ctx: &'ctx InferContext<'db, 'ctx>,
|
||||||
lint: &'static LintMetadata,
|
lint: LintId,
|
||||||
) -> Option<(Severity, LintSource)> {
|
) -> Option<(Severity, LintSource)> {
|
||||||
// The comment below was copied from the original
|
// The comment below was copied from the original
|
||||||
// implementation of diagnostic reporting. The code
|
// implementation of diagnostic reporting. The code
|
||||||
|
|
@ -420,10 +420,9 @@ impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> {
|
||||||
if !ctx.db.should_check_file(ctx.file) {
|
if !ctx.db.should_check_file(ctx.file) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let lint_id = LintId::of(lint);
|
|
||||||
// Skip over diagnostics if the rule
|
// Skip over diagnostics if the rule
|
||||||
// is disabled.
|
// is disabled.
|
||||||
let (severity, source) = ctx.db.rule_selection(ctx.file).get(lint_id)?;
|
let (severity, source) = ctx.db.rule_selection(ctx.file).get(lint)?;
|
||||||
// If we're not in type checking mode,
|
// If we're not in type checking mode,
|
||||||
// we can bail now.
|
// we can bail now.
|
||||||
if ctx.is_in_no_type_check() {
|
if ctx.is_in_no_type_check() {
|
||||||
|
|
@ -443,20 +442,20 @@ impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> {
|
||||||
lint: &'static LintMetadata,
|
lint: &'static LintMetadata,
|
||||||
range: TextRange,
|
range: TextRange,
|
||||||
) -> Option<LintDiagnosticGuardBuilder<'db, 'ctx>> {
|
) -> Option<LintDiagnosticGuardBuilder<'db, 'ctx>> {
|
||||||
let (severity, source) = Self::severity_and_source(ctx, lint)?;
|
let lint_id = LintId::of(lint);
|
||||||
|
|
||||||
|
let (severity, source) = Self::severity_and_source(ctx, lint_id)?;
|
||||||
|
|
||||||
let suppressions = suppressions(ctx.db(), ctx.file());
|
let suppressions = suppressions(ctx.db(), ctx.file());
|
||||||
let lint_id = LintId::of(lint);
|
|
||||||
if let Some(suppression) = suppressions.find_suppression(range, lint_id) {
|
if let Some(suppression) = suppressions.find_suppression(range, lint_id) {
|
||||||
ctx.diagnostics.borrow_mut().mark_used(suppression.id());
|
ctx.diagnostics.borrow_mut().mark_used(suppression.id());
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let id = DiagnosticId::Lint(lint.name());
|
|
||||||
let primary_span = Span::from(ctx.file()).with_range(range);
|
let primary_span = Span::from(ctx.file()).with_range(range);
|
||||||
Some(LintDiagnosticGuardBuilder {
|
Some(LintDiagnosticGuardBuilder {
|
||||||
ctx,
|
ctx,
|
||||||
id,
|
id: lint_id,
|
||||||
severity,
|
severity,
|
||||||
source,
|
source,
|
||||||
primary_span,
|
primary_span,
|
||||||
|
|
@ -477,7 +476,8 @@ impl<'db, 'ctx> LintDiagnosticGuardBuilder<'db, 'ctx> {
|
||||||
self,
|
self,
|
||||||
message: impl std::fmt::Display,
|
message: impl std::fmt::Display,
|
||||||
) -> LintDiagnosticGuard<'db, 'ctx> {
|
) -> LintDiagnosticGuard<'db, 'ctx> {
|
||||||
let mut diag = Diagnostic::new(self.id, self.severity, message);
|
let mut diag = Diagnostic::new(DiagnosticId::Lint(self.id.name()), self.severity, message);
|
||||||
|
diag.set_documentation_url(Some(self.id.documentation_url()));
|
||||||
// This is why `LintDiagnosticGuard::set_primary_message` exists.
|
// This is why `LintDiagnosticGuard::set_primary_message` exists.
|
||||||
// We add the primary annotation here (because it's required), but
|
// We add the primary annotation here (because it's required), but
|
||||||
// the optional message can be added later. We could accept it here
|
// the optional message can be added later. We could accept it here
|
||||||
|
|
@ -629,10 +629,15 @@ impl<'db, 'ctx> DiagnosticGuardBuilder<'db, 'ctx> {
|
||||||
self,
|
self,
|
||||||
message: impl std::fmt::Display,
|
message: impl std::fmt::Display,
|
||||||
) -> DiagnosticGuard<'db, 'ctx> {
|
) -> DiagnosticGuard<'db, 'ctx> {
|
||||||
let diag = Some(Diagnostic::new(self.id, self.severity, message));
|
let mut diag = Diagnostic::new(self.id, self.severity, message);
|
||||||
|
|
||||||
|
if let DiagnosticId::Lint(lint_name) = diag.id() {
|
||||||
|
diag.set_documentation_url(Some(lint_documentation_url(lint_name)));
|
||||||
|
}
|
||||||
|
|
||||||
DiagnosticGuard {
|
DiagnosticGuard {
|
||||||
ctx: self.ctx,
|
ctx: self.ctx,
|
||||||
diag,
|
diag: Some(diag),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -320,15 +320,11 @@ pub(super) fn to_lsp_diagnostic(
|
||||||
})
|
})
|
||||||
.filter(|mapped_tags| !mapped_tags.is_empty());
|
.filter(|mapped_tags| !mapped_tags.is_empty());
|
||||||
|
|
||||||
let code_description = diagnostic
|
let code_description = diagnostic.documentation_url().and_then(|url| {
|
||||||
.id()
|
let href = Url::parse(url).ok()?;
|
||||||
.is_lint()
|
|
||||||
.then(|| {
|
Some(CodeDescription { href })
|
||||||
Some(CodeDescription {
|
});
|
||||||
href: Url::parse(&format!("https://ty.dev/rules#{}", diagnostic.id())).ok()?,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.flatten();
|
|
||||||
|
|
||||||
let mut related_information = Vec::new();
|
let mut related_information = Vec::new();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue