feat(linter): add hyperlinks to diagnostic messages (#5318)

Adds hyperlinks to diagnostic codes.

Diagnostics without codes will not have links. In practice, this means linter diagnostics have links, while semantic and parser diagnostics do not.

![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/OVwvhVoSj4tgQWTUipoY/11790a3a-7dfa-4c6d-be43-550d8b6370a9.png)

> notice the underline under the error code
This commit is contained in:
DonIsaac 2024-08-29 05:50:04 +00:00
parent cd63336c7e
commit 9c22ce9c99
4 changed files with 52 additions and 15 deletions

View file

@ -271,12 +271,24 @@ impl GraphicalReportHandler {
opts = opts.word_splitter(word_splitter);
}
let title = if let Some(code) = diagnostic.code() {
format!("{code}: {diagnostic}")
} else {
diagnostic.to_string()
let title = match (self.links, diagnostic.url(), diagnostic.code()) {
(LinkStyle::Link, Some(url), Some(code)) => {
// magic unicode escape sequences to make the terminal print a hyperlink
const CTL: &str = "\u{1b}]8;;";
const END: &str = "\u{1b}]8;;\u{1b}\\";
let code = code.style(severity_style);
let message = diagnostic.to_string();
let title = message.style(severity_style);
format!("{CTL}{url}\u{1b}\\{code}{END}: {title}",)
}
(_, _, Some(code)) => {
let title = format!("{code}: {}", diagnostic);
format!("{}", title.style(severity_style))
}
_ => {
format!("{}", diagnostic.to_string().style(severity_style))
}
};
let title = format!("{}", title.style(severity_style));
let title = textwrap::fill(&title, opts);
writeln!(f, "{}", title)?;

View file

@ -71,6 +71,7 @@ pub struct OxcDiagnosticInner {
pub help: Option<Cow<'static, str>>,
pub severity: Severity,
pub code: OxcCode,
pub url: Option<Cow<'static, str>>,
}
impl fmt::Display for OxcDiagnostic {
@ -101,6 +102,9 @@ impl Diagnostic for OxcDiagnostic {
fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
self.code.is_some().then(|| Box::new(&self.code) as Box<dyn Display>)
}
fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
self.url.as_ref().map(Box::new).map(|c| c as Box<dyn Display>)
}
}
impl OxcDiagnostic {
@ -112,6 +116,7 @@ impl OxcDiagnostic {
help: None,
severity: Severity::Error,
code: OxcCode::default(),
url: None,
}),
}
}
@ -124,6 +129,7 @@ impl OxcDiagnostic {
help: None,
severity: Severity::Warning,
code: OxcCode::default(),
url: None,
}),
}
}
@ -205,6 +211,11 @@ impl OxcDiagnostic {
self
}
pub fn with_url<S: Into<Cow<'static, str>>>(mut self, url: S) -> Self {
self.inner.url = Some(url.into());
self
}
pub fn with_source_code<T: SourceCode + Send + Sync + 'static>(self, code: T) -> Error {
Error::from(self).with_source_code(code)
}

View file

@ -41,6 +41,7 @@ pub struct LintContext<'a> {
eslint_config: Arc<OxlintConfig>,
// states
current_plugin_name: &'static str,
current_plugin_prefix: &'static str,
current_rule_name: &'static str,
#[cfg(debug_assertions)]
@ -60,6 +61,7 @@ pub struct LintContext<'a> {
}
impl<'a> LintContext<'a> {
const WEBSITE_BASE_URL: &'static str = "https://oxc.rs/docs/guide/usage/linter/rules";
/// # Panics
/// If `semantic.cfg()` is `None`.
pub fn new(file_path: Box<Path>, semantic: Rc<Semantic<'a>>) -> Self {
@ -81,6 +83,7 @@ impl<'a> LintContext<'a> {
fix: FixKind::None,
file_path: file_path.into(),
eslint_config: Arc::new(OxlintConfig::default()),
current_plugin_name: "eslint",
current_plugin_prefix: "eslint",
current_rule_name: "",
#[cfg(debug_assertions)]
@ -102,6 +105,7 @@ impl<'a> LintContext<'a> {
}
pub fn with_plugin_name(mut self, plugin: &'static str) -> Self {
self.current_plugin_name = plugin;
self.current_plugin_prefix = plugin_name_to_prefix(plugin);
self
}
@ -209,16 +213,24 @@ impl<'a> LintContext<'a> {
self.diagnostics.borrow().iter().cloned().collect::<Vec<_>>()
}
fn add_diagnostic(&self, message: Message<'a>) {
if !self.disable_directives.contains(self.current_rule_name, message.span()) {
let mut message = message;
message.error =
message.error.with_error_code(self.current_plugin_prefix, self.current_rule_name);
if message.error.severity != self.severity {
message.error = message.error.with_severity(self.severity);
}
self.diagnostics.borrow_mut().push(message);
fn add_diagnostic(&self, mut message: Message<'a>) {
if self.disable_directives.contains(self.current_rule_name, message.span()) {
return;
}
message.error = message
.error
.with_error_code(self.current_plugin_prefix, self.current_rule_name)
.with_url(format!(
"{}/{}/{}.html",
Self::WEBSITE_BASE_URL,
self.current_plugin_name,
self.current_rule_name
));
if message.error.severity != self.severity {
message.error = message.error.with_severity(self.severity);
}
self.diagnostics.borrow_mut().push(message);
}
/// Report a lint rule violation.

View file

@ -394,7 +394,9 @@ impl Tester {
}
.to_string_lossy();
let handler = GraphicalReportHandler::new().with_theme(GraphicalTheme::unicode_nocolor());
let handler = GraphicalReportHandler::new()
.with_links(false)
.with_theme(GraphicalTheme::unicode_nocolor());
for diagnostic in result {
let diagnostic = diagnostic.error.with_source_code(NamedSource::new(
diagnostic_path.clone(),