[ty] Add hyperlinks to rule codes in CLI (#21502)

This commit is contained in:
Micha Reiser 2025-11-18 16:36:59 +01:00 committed by GitHub
parent 5ca9c15fc8
commit 7043d51df0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 195 additions and 47 deletions

View File

@ -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 }
}

View File

@ -76,6 +76,7 @@ impl Renderer {
} }
.effects(Effects::BOLD), .effects(Effects::BOLD),
none: Style::new(), none: Style::new(),
hyperlink: true,
}, },
..Self::plain() ..Self::plain()
} }

View File

@ -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,
} }
} }
} }

View File

@ -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
} }

View File

@ -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>,

View File

@ -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)
} }

View File

@ -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
)
)?; )?;
} }

View File

@ -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,
} }
} }
} }

View File

@ -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,

View File

@ -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 {

View File

@ -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);

View File

@ -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),
} }
} }
} }

View File

@ -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();