From 0645418f002d0743611dbf26fa6131e9717c3c5b Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Tue, 18 Nov 2025 13:34:50 -0500 Subject: [PATCH] Set the diagnostic URL for lint errors (#21514) Summary -- This PR wires up the `Diagnostic::set_documentation_url` method from #21502 to Ruff's lint diagnostics. This enables the links for the full and concise output formats without any other changes. I considered also including the URLs for the grouped and pylint output formats, but the grouped format is still in `ruff_linter` instead of `ruff_db`, so we'd have to export some additional functionality to wire it up with `fmt_with_hyperlink`; and the pylint format doesn't currently render with color, so I think it might actually be machine readable rather than human readable? The other ouput formats (json, json-lines, junit, github, gitlab, rdjson, azure, sarif) seem more clearly not to need the links. Test Plan -- I guess you can't see my cursor or the browser opening, but it works for lint rules, which have links, and doesn't include a link for syntax errors, which don't have valid links. ![out](https://github.com/user-attachments/assets/a520c7f9-6d7b-4e5f-a1a9-3c5e21a51d3c) --- crates/ruff_db/src/diagnostic/mod.rs | 22 ------------------- crates/ruff_db/src/diagnostic/render.rs | 12 ++++++++++ crates/ruff_db/src/diagnostic/render/json.rs | 16 +++++++++----- .../ruff_db/src/diagnostic/render/rdjson.rs | 14 ++++++++---- crates/ruff_linter/src/message/mod.rs | 1 + crates/ruff_server/src/lint.rs | 4 ++-- 6 files changed, 36 insertions(+), 33 deletions(-) diff --git a/crates/ruff_db/src/diagnostic/mod.rs b/crates/ruff_db/src/diagnostic/mod.rs index b9ba21e5c4..a8f2d09dd8 100644 --- a/crates/ruff_db/src/diagnostic/mod.rs +++ b/crates/ruff_db/src/diagnostic/mod.rs @@ -452,28 +452,6 @@ impl Diagnostic { .map(|sub| sub.inner.message.as_str()) } - /// Returns the URL for the rule documentation, if it exists. - pub fn to_ruff_url(&self) -> Option { - match self.id() { - DiagnosticId::Panic - | DiagnosticId::Io - | DiagnosticId::InvalidSyntax - | DiagnosticId::RevealedType - | DiagnosticId::UnknownRule - | DiagnosticId::InvalidGlob - | DiagnosticId::EmptyInclude - | DiagnosticId::UnnecessaryOverridesSection - | DiagnosticId::UselessOverridesSection - | DiagnosticId::DeprecatedSetting - | DiagnosticId::Unformatted - | DiagnosticId::InvalidCliOption - | DiagnosticId::InternalError => None, - DiagnosticId::Lint(lint_name) => { - Some(format!("{}/rules/{lint_name}", env!("CARGO_PKG_HOMEPAGE"))) - } - } - } - /// Returns the filename for the message. /// /// Panics if the diagnostic has no primary span, or if its file is not a `SourceFile`. diff --git a/crates/ruff_db/src/diagnostic/render.rs b/crates/ruff_db/src/diagnostic/render.rs index dd55fcc2d7..d9f8775ef2 100644 --- a/crates/ruff_db/src/diagnostic/render.rs +++ b/crates/ruff_db/src/diagnostic/render.rs @@ -2881,6 +2881,12 @@ watermelon self.diag.help(message); self } + + /// Set the documentation URL for the diagnostic. + pub(super) fn documentation_url(mut self, url: impl Into) -> DiagnosticBuilder<'e> { + self.diag.set_documentation_url(Some(url.into())); + self + } } /// A helper builder for tersely populating a `SubDiagnostic`. @@ -2995,6 +3001,7 @@ def fibonacci(n): TextSize::from(10), )))) .noqa_offset(TextSize::from(7)) + .documentation_url("https://docs.astral.sh/ruff/rules/unused-import") .build(), env.builder( "unused-variable", @@ -3009,11 +3016,13 @@ def fibonacci(n): 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(), ]; @@ -3128,6 +3137,7 @@ if call(foo TextSize::from(19), )))) .noqa_offset(TextSize::from(16)) + .documentation_url("https://docs.astral.sh/ruff/rules/unused-import") .build(), env.builder( "unused-import", @@ -3142,6 +3152,7 @@ if call(foo TextSize::from(40), )))) .noqa_offset(TextSize::from(35)) + .documentation_url("https://docs.astral.sh/ruff/rules/unused-import") .build(), env.builder( "unused-variable", @@ -3156,6 +3167,7 @@ if call(foo TextSize::from(104), )))) .noqa_offset(TextSize::from(98)) + .documentation_url("https://docs.astral.sh/ruff/rules/unused-variable") .build(), ]; diff --git a/crates/ruff_db/src/diagnostic/render/json.rs b/crates/ruff_db/src/diagnostic/render/json.rs index e31af45396..2a82ec39db 100644 --- a/crates/ruff_db/src/diagnostic/render/json.rs +++ b/crates/ruff_db/src/diagnostic/render/json.rs @@ -100,7 +100,7 @@ pub(super) fn diagnostic_to_json<'a>( if config.preview { JsonDiagnostic { code: diagnostic.secondary_code_or_id(), - url: diagnostic.to_ruff_url(), + url: diagnostic.documentation_url(), message: diagnostic.body(), fix, cell: notebook_cell_index, @@ -112,7 +112,7 @@ pub(super) fn diagnostic_to_json<'a>( } else { JsonDiagnostic { code: diagnostic.secondary_code_or_id(), - url: diagnostic.to_ruff_url(), + url: diagnostic.documentation_url(), message: diagnostic.body(), fix, cell: notebook_cell_index, @@ -228,7 +228,7 @@ pub(crate) struct JsonDiagnostic<'a> { location: Option, message: &'a str, noqa_row: Option, - url: Option, + url: Option<&'a str>, } #[derive(Serialize)] @@ -294,7 +294,10 @@ mod tests { env.format(DiagnosticFormat::Json); env.preview(false); - let diag = env.err().build(); + let diag = env + .err() + .documentation_url("https://docs.astral.sh/ruff/rules/test-diagnostic") + .build(); insta::assert_snapshot!( env.render(&diag), @@ -328,7 +331,10 @@ mod tests { env.format(DiagnosticFormat::Json); env.preview(true); - let diag = env.err().build(); + let diag = env + .err() + .documentation_url("https://docs.astral.sh/ruff/rules/test-diagnostic") + .build(); insta::assert_snapshot!( env.render(&diag), diff --git a/crates/ruff_db/src/diagnostic/render/rdjson.rs b/crates/ruff_db/src/diagnostic/render/rdjson.rs index bfff72071b..e2a976aa7d 100644 --- a/crates/ruff_db/src/diagnostic/render/rdjson.rs +++ b/crates/ruff_db/src/diagnostic/render/rdjson.rs @@ -82,7 +82,7 @@ fn diagnostic_to_rdjson<'a>( value: diagnostic .secondary_code() .map_or_else(|| diagnostic.name(), |code| code.as_str()), - url: diagnostic.to_ruff_url(), + url: diagnostic.documentation_url(), }, suggestions: rdjson_suggestions( edits, @@ -182,7 +182,7 @@ impl RdjsonRange { #[derive(Serialize)] struct RdjsonCode<'a> { #[serde(skip_serializing_if = "Option::is_none")] - url: Option, + url: Option<&'a str>, value: &'a str, } @@ -217,7 +217,10 @@ mod tests { env.format(DiagnosticFormat::Rdjson); env.preview(false); - let diag = env.err().build(); + let diag = env + .err() + .documentation_url("https://docs.astral.sh/ruff/rules/test-diagnostic") + .build(); insta::assert_snapshot!(env.render(&diag)); } @@ -228,7 +231,10 @@ mod tests { env.format(DiagnosticFormat::Rdjson); env.preview(true); - let diag = env.err().build(); + let diag = env + .err() + .documentation_url("https://docs.astral.sh/ruff/rules/test-diagnostic") + .build(); insta::assert_snapshot!(env.render(&diag)); } diff --git a/crates/ruff_linter/src/message/mod.rs b/crates/ruff_linter/src/message/mod.rs index 2525322fd9..e5494432cf 100644 --- a/crates/ruff_linter/src/message/mod.rs +++ b/crates/ruff_linter/src/message/mod.rs @@ -125,6 +125,7 @@ where } diagnostic.set_secondary_code(SecondaryCode::new(rule.noqa_code().to_string())); + diagnostic.set_documentation_url(rule.url()); diagnostic } diff --git a/crates/ruff_server/src/lint.rs b/crates/ruff_server/src/lint.rs index 073587a386..665e303b39 100644 --- a/crates/ruff_server/src/lint.rs +++ b/crates/ruff_server/src/lint.rs @@ -301,9 +301,9 @@ fn to_lsp_diagnostic( severity, tags, code, - code_description: diagnostic.to_ruff_url().and_then(|url| { + code_description: diagnostic.documentation_url().and_then(|url| { Some(lsp_types::CodeDescription { - href: lsp_types::Url::parse(&url).ok()?, + href: lsp_types::Url::parse(url).ok()?, }) }), source: Some(DIAGNOSTIC_NAME.into()),