## Summary This PR wires up the GitHub output format moved to `ruff_db` in #20320 to the ty CLI. It's a bit smaller than the GitLab version (#20155) because some of the helpers were already in place, but I did factor out a few `DisplayDiagnosticConfig` constructor calls in Ruff. I also exposed the `GithubRenderer` and a wrapper `DisplayGithubDiagnostics` type because we needed a way to configure the program name displayed in the GitHub diagnostics. This was previously hard-coded to `Ruff`: <img width="675" height="247" alt="image" src="https://github.com/user-attachments/assets/592da860-d2f5-4abd-bc5a-66071d742509" /> Another option would be to drop the program name in the output format, but I think it can be helpful in workflows with multiple programs emitting annotations (such as Ruff and ty!) ## Test Plan New CLI test, and a manual test with `--config 'terminal.output-format = "github"'`
137 lines
4.2 KiB
Rust
137 lines
4.2 KiB
Rust
use crate::diagnostic::{Diagnostic, FileResolver, Severity};
|
|
|
|
pub struct GithubRenderer<'a> {
|
|
resolver: &'a dyn FileResolver,
|
|
program: &'a str,
|
|
}
|
|
|
|
impl<'a> GithubRenderer<'a> {
|
|
pub fn new(resolver: &'a dyn FileResolver, program: &'a str) -> Self {
|
|
Self { resolver, program }
|
|
}
|
|
|
|
pub(super) fn render(
|
|
&self,
|
|
f: &mut std::fmt::Formatter,
|
|
diagnostics: &[Diagnostic],
|
|
) -> std::fmt::Result {
|
|
for diagnostic in diagnostics {
|
|
let severity = match diagnostic.severity() {
|
|
Severity::Info => "notice",
|
|
Severity::Warning => "warning",
|
|
Severity::Error | Severity::Fatal => "error",
|
|
};
|
|
write!(
|
|
f,
|
|
"::{severity} title={program} ({code})",
|
|
program = self.program,
|
|
code = diagnostic.secondary_code_or_id()
|
|
)?;
|
|
|
|
if let Some(span) = diagnostic.primary_span() {
|
|
let file = span.file();
|
|
write!(f, ",file={file}", file = file.path(self.resolver))?;
|
|
|
|
let (start_location, end_location) = if self.resolver.is_notebook(file) {
|
|
// We can't give a reasonable location for the structured formats,
|
|
// so we show one that's clearly a fallback
|
|
None
|
|
} else {
|
|
let diagnostic_source = file.diagnostic_source(self.resolver);
|
|
let source_code = diagnostic_source.as_source_code();
|
|
|
|
span.range().map(|range| {
|
|
(
|
|
source_code.line_column(range.start()),
|
|
source_code.line_column(range.end()),
|
|
)
|
|
})
|
|
}
|
|
.unwrap_or_default();
|
|
|
|
write!(
|
|
f,
|
|
",line={row},col={column},endLine={end_row},endColumn={end_column}::",
|
|
row = start_location.line,
|
|
column = start_location.column,
|
|
end_row = end_location.line,
|
|
end_column = end_location.column,
|
|
)?;
|
|
|
|
write!(
|
|
f,
|
|
"{path}:{row}:{column}: ",
|
|
path = file.relative_path(self.resolver).display(),
|
|
row = start_location.line,
|
|
column = start_location.column,
|
|
)?;
|
|
} else {
|
|
write!(f, "::")?;
|
|
}
|
|
|
|
if let Some(code) = diagnostic.secondary_code() {
|
|
write!(f, "{code}")?;
|
|
} else {
|
|
write!(f, "{id}:", id = diagnostic.id())?;
|
|
}
|
|
|
|
writeln!(f, " {}", diagnostic.body())?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub struct DisplayGithubDiagnostics<'a> {
|
|
renderer: &'a GithubRenderer<'a>,
|
|
diagnostics: &'a [Diagnostic],
|
|
}
|
|
|
|
impl<'a> DisplayGithubDiagnostics<'a> {
|
|
pub fn new(renderer: &'a GithubRenderer<'a>, diagnostics: &'a [Diagnostic]) -> Self {
|
|
Self {
|
|
renderer,
|
|
diagnostics,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for DisplayGithubDiagnostics<'_> {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
self.renderer.render(f, self.diagnostics)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::diagnostic::{
|
|
DiagnosticFormat,
|
|
render::tests::{TestEnvironment, create_diagnostics, create_syntax_error_diagnostics},
|
|
};
|
|
|
|
#[test]
|
|
fn output() {
|
|
let (env, diagnostics) = create_diagnostics(DiagnosticFormat::Github);
|
|
insta::assert_snapshot!(env.render_diagnostics(&diagnostics));
|
|
}
|
|
|
|
#[test]
|
|
fn syntax_errors() {
|
|
let (env, diagnostics) = create_syntax_error_diagnostics(DiagnosticFormat::Github);
|
|
insta::assert_snapshot!(env.render_diagnostics(&diagnostics));
|
|
}
|
|
|
|
#[test]
|
|
fn missing_file() {
|
|
let mut env = TestEnvironment::new();
|
|
env.format(DiagnosticFormat::Github);
|
|
|
|
let diag = env.err().build();
|
|
|
|
insta::assert_snapshot!(
|
|
env.render(&diag),
|
|
@"::error title=ty (test-diagnostic)::test-diagnostic: main diagnostic message",
|
|
);
|
|
}
|
|
}
|