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)
This commit is contained in:
Brent Westbrook 2025-11-18 13:34:50 -05:00 committed by GitHub
parent 62343a101a
commit 0645418f00
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 36 additions and 33 deletions

View File

@ -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<String> {
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`.

View File

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

View File

@ -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<JsonLocation>,
message: &'a str,
noqa_row: Option<OneIndexed>,
url: Option<String>,
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),

View File

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

View File

@ -125,6 +125,7 @@ where
}
diagnostic.set_secondary_code(SecondaryCode::new(rule.noqa_code().to_string()));
diagnostic.set_documentation_url(rule.url());
diagnostic
}

View File

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