mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
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.  > notice the underline under the error code
This commit is contained in:
parent
cd63336c7e
commit
9c22ce9c99
4 changed files with 52 additions and 15 deletions
|
|
@ -271,12 +271,24 @@ impl GraphicalReportHandler {
|
||||||
opts = opts.word_splitter(word_splitter);
|
opts = opts.word_splitter(word_splitter);
|
||||||
}
|
}
|
||||||
|
|
||||||
let title = if let Some(code) = diagnostic.code() {
|
let title = match (self.links, diagnostic.url(), diagnostic.code()) {
|
||||||
format!("{code}: {diagnostic}")
|
(LinkStyle::Link, Some(url), Some(code)) => {
|
||||||
} else {
|
// magic unicode escape sequences to make the terminal print a hyperlink
|
||||||
diagnostic.to_string()
|
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);
|
let title = textwrap::fill(&title, opts);
|
||||||
writeln!(f, "{}", title)?;
|
writeln!(f, "{}", title)?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ pub struct OxcDiagnosticInner {
|
||||||
pub help: Option<Cow<'static, str>>,
|
pub help: Option<Cow<'static, str>>,
|
||||||
pub severity: Severity,
|
pub severity: Severity,
|
||||||
pub code: OxcCode,
|
pub code: OxcCode,
|
||||||
|
pub url: Option<Cow<'static, str>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for OxcDiagnostic {
|
impl fmt::Display for OxcDiagnostic {
|
||||||
|
|
@ -101,6 +102,9 @@ impl Diagnostic for OxcDiagnostic {
|
||||||
fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
|
fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
|
||||||
self.code.is_some().then(|| Box::new(&self.code) as Box<dyn Display>)
|
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 {
|
impl OxcDiagnostic {
|
||||||
|
|
@ -112,6 +116,7 @@ impl OxcDiagnostic {
|
||||||
help: None,
|
help: None,
|
||||||
severity: Severity::Error,
|
severity: Severity::Error,
|
||||||
code: OxcCode::default(),
|
code: OxcCode::default(),
|
||||||
|
url: None,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -124,6 +129,7 @@ impl OxcDiagnostic {
|
||||||
help: None,
|
help: None,
|
||||||
severity: Severity::Warning,
|
severity: Severity::Warning,
|
||||||
code: OxcCode::default(),
|
code: OxcCode::default(),
|
||||||
|
url: None,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -205,6 +211,11 @@ impl OxcDiagnostic {
|
||||||
self
|
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 {
|
pub fn with_source_code<T: SourceCode + Send + Sync + 'static>(self, code: T) -> Error {
|
||||||
Error::from(self).with_source_code(code)
|
Error::from(self).with_source_code(code)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ pub struct LintContext<'a> {
|
||||||
eslint_config: Arc<OxlintConfig>,
|
eslint_config: Arc<OxlintConfig>,
|
||||||
|
|
||||||
// states
|
// states
|
||||||
|
current_plugin_name: &'static str,
|
||||||
current_plugin_prefix: &'static str,
|
current_plugin_prefix: &'static str,
|
||||||
current_rule_name: &'static str,
|
current_rule_name: &'static str,
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
|
|
@ -60,6 +61,7 @@ pub struct LintContext<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> LintContext<'a> {
|
impl<'a> LintContext<'a> {
|
||||||
|
const WEBSITE_BASE_URL: &'static str = "https://oxc.rs/docs/guide/usage/linter/rules";
|
||||||
/// # Panics
|
/// # Panics
|
||||||
/// If `semantic.cfg()` is `None`.
|
/// If `semantic.cfg()` is `None`.
|
||||||
pub fn new(file_path: Box<Path>, semantic: Rc<Semantic<'a>>) -> Self {
|
pub fn new(file_path: Box<Path>, semantic: Rc<Semantic<'a>>) -> Self {
|
||||||
|
|
@ -81,6 +83,7 @@ impl<'a> LintContext<'a> {
|
||||||
fix: FixKind::None,
|
fix: FixKind::None,
|
||||||
file_path: file_path.into(),
|
file_path: file_path.into(),
|
||||||
eslint_config: Arc::new(OxlintConfig::default()),
|
eslint_config: Arc::new(OxlintConfig::default()),
|
||||||
|
current_plugin_name: "eslint",
|
||||||
current_plugin_prefix: "eslint",
|
current_plugin_prefix: "eslint",
|
||||||
current_rule_name: "",
|
current_rule_name: "",
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
|
|
@ -102,6 +105,7 @@ impl<'a> LintContext<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_plugin_name(mut self, plugin: &'static str) -> Self {
|
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.current_plugin_prefix = plugin_name_to_prefix(plugin);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
@ -209,16 +213,24 @@ impl<'a> LintContext<'a> {
|
||||||
self.diagnostics.borrow().iter().cloned().collect::<Vec<_>>()
|
self.diagnostics.borrow().iter().cloned().collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_diagnostic(&self, message: Message<'a>) {
|
fn add_diagnostic(&self, mut message: Message<'a>) {
|
||||||
if !self.disable_directives.contains(self.current_rule_name, message.span()) {
|
if self.disable_directives.contains(self.current_rule_name, message.span()) {
|
||||||
let mut message = message;
|
return;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
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.
|
/// Report a lint rule violation.
|
||||||
|
|
|
||||||
|
|
@ -394,7 +394,9 @@ impl Tester {
|
||||||
}
|
}
|
||||||
.to_string_lossy();
|
.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 {
|
for diagnostic in result {
|
||||||
let diagnostic = diagnostic.error.with_source_code(NamedSource::new(
|
let diagnostic = diagnostic.error.with_source_code(NamedSource::new(
|
||||||
diagnostic_path.clone(),
|
diagnostic_path.clone(),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue