From 8666f1efb5e490e1b3c0bed5fc08b6f79cc2ca65 Mon Sep 17 00:00:00 2001 From: m Date: Thu, 19 Sep 2024 18:40:15 -0700 Subject: [PATCH] current work on annotate-snippets --- Cargo.lock | 19 ++- Cargo.toml | 1 + src/check_release.rs | 284 +++++++++++++++++++-------------- src/lints/function_missing.ron | 2 +- 4 files changed, 188 insertions(+), 118 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 944b914b..5709d1ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,6 +44,16 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[package]] +name = "annotate-snippets" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24e35ed54e5ea7997c14ed4c70ba043478db1112e98263b3b035907aa197d991" +dependencies = [ + "anstyle", + "unicode-width", +] + [[package]] name = "anstream" version = "0.6.15" @@ -321,6 +331,7 @@ dependencies = [ name = "cargo-semver-checks" version = "0.35.0" dependencies = [ + "annotate-snippets", "anstream", "anstyle", "anyhow", @@ -3719,6 +3730,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "untrusted" version = "0.9.0" @@ -3911,7 +3928,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 21858265..b345d8d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ anstyle = "1.0.8" anstream = "0.6.15" urlencoding = "2.1.3" cargo-config2 = "0.1.29" +annotate-snippets = "0.11.4" [dev-dependencies] assert_cmd = "2.0" diff --git a/src/check_release.rs b/src/check_release.rs index 79d139ba..ce40572d 100644 --- a/src/check_release.rs +++ b/src/check_release.rs @@ -1,6 +1,7 @@ use std::io::Write as _; use std::{collections::BTreeMap, sync::Arc, time::Instant}; +use annotate_snippets::{Level, Renderer, Snippet}; use anstyle::{AnsiColor, Color, Reset, Style}; use anyhow::Context; @@ -70,111 +71,169 @@ fn print_triggered_lint( semver_query: &SemverQuery, results: Vec, FieldValue>>, witness_generation: &WitnessGeneration, + level: LintLevel, ) -> anyhow::Result<()> { + let snippets_level = match level { + LintLevel::Deny => Level::Error, + LintLevel::Warn => Level::Warning, + LintLevel::Allow => unreachable!("Called `print_triggered_lint` on an `allow`-level lint"), + }; + + let title = format!("{}: {}", semver_query.id, semver_query.human_readable_name); + let mut message = snippets_level.title(&title); + let mut description = Level::Info.title(&semver_query.error_message); + if let Some(ref_link) = semver_query.reference_link.as_deref() { - config.log_info(|config| { - writeln!(config.stdout(), "{}Description:{}\n{}\n{:>12} {}\n{:>12} https://github.com/obi1kenobi/cargo-semver-checks/tree/v{}/src/lints/{}.ron\n", - Style::new().bold(), Reset, - &semver_query.error_message, - "ref:", - ref_link, - "impl:", - crate_version!(), - semver_query.id, - )?; - Ok(()) - })?; - } else { - config.log_info(|config| { - writeln!( - config.stdout(), - "{}Description:{}\n{}\n{:>12} https://github.com/obi1kenobi/cargo-semver-checks/tree/v{}/src/lints/{}.ron", - Style::new().bold(), - Reset, - &semver_query.error_message, - "impl:", - crate_version!(), - semver_query.id, - )?; - Ok(()) - })?; + description = description.footer(Level::Info.title(ref_link).id("ref")); } - config.log_info(|config| { - writeln!( - config.stdout(), - "{}Failed in:{}", - Style::new().bold(), - Reset - )?; - Ok(()) - })?; - - for semver_violation_result in results { - let pretty_result: BTreeMap, TransparentValue> = semver_violation_result - .into_iter() - .map(|(k, v)| (k, v.into())) - .collect(); - - if let Some(template) = semver_query.per_result_error_template.as_deref() { - let message = config - .handlebars() - .render_template(template, &pretty_result) - .context("Error instantiating semver query template.") - .expect("could not materialize template"); - config.log_info(|config| { - writeln!(config.stdout(), " {}", message)?; - Ok(()) - })?; - - config.log_extra_verbose(|config| { - let serde_pretty = - serde_json::to_string_pretty(&pretty_result).expect("serde failed"); - let indented_serde = serde_pretty - .split('\n') - .map(|line| format!(" {line}")) - .join("\n"); - writeln!( - config.stdout(), - "\tlint rule output values:\n{}", - indented_serde - )?; - Ok(()) - })?; + let impl_link = format!( + "https://github.com/obi1kenobi/cargo-semver-checks/tree/v{}/src/lints/{}.ron", + crate_version!(), + semver_query.id + ); + description = description.footer(Level::Info.title(&impl_link).id("impl")); + + message = message.footer(description); + + #[derive(Debug, Default)] + struct ResultFormat { + error_message: Option, + /// (text source, 1-indexed line number to error, filename) + span: Option<(String, usize, String)>, + serde_pretty: Option, + witness_hint: Option, + } + + let formats: Vec<_> = results + .into_iter() + .map(|semver_violation_result| { + let mut result = ResultFormat::default(); + + if let Some(filename) = semver_violation_result + .get("span_filename") + .and_then(FieldValue::as_str) + { + if let Some(begin_line) = semver_violation_result + .get("span_begin_line") + .and_then(FieldValue::as_usize) + { + match std::fs::read_to_string(filename) { + Ok(contents) => { + result.span = Some((contents, begin_line, filename.to_owned())); + } + Err(e) => config + .log_verbose(|config| { + config.shell_warn(format_args!( + "could not read file `{filename}`: {e}" + )) + }) + .expect("printing failed"), + } + } + } + + let pretty_result: BTreeMap, TransparentValue> = semver_violation_result + .into_iter() + .map(|(k, v)| (k, v.into())) + .collect(); + + if let Some(template) = semver_query.per_result_error_template.as_deref() { + result.error_message = Some( + config + .handlebars() + .render_template(template, &pretty_result) + .context("Error instantiating semver query template.") + .expect("could not materialize template"), + ); + + if config.is_extra_verbose() { + result.serde_pretty = Some( + serde_json::to_string_pretty(&pretty_result) + .expect("error serializing pretty results"), + ); + } + } else { + result.serde_pretty = Some( + serde_json::to_string_pretty(&pretty_result) + .expect("error serializing pretty results"), + ); + } + + if let Some(witness) = &semver_query.witness { + if witness_generation.show_hints { + result.witness_hint = Some( + config + .handlebars() + .render_template(&witness.hint_template, &pretty_result) + .expect("Error instantiating witness hint template."), + ); + } + } + result + }) + .collect(); + + for format_result in &formats { + let mut failure = if let Some(error_message) = &format_result.error_message { + snippets_level.title(error_message).id("failed in") } else { - config.log_info(|config| { - writeln!( - config.stdout(), - "{}\n", - serde_json::to_string_pretty(&pretty_result)? - )?; - Ok(()) - })?; + snippets_level.title("failed in:") + }; + + if let Some((source, begin_line, filename)) = &format_result.span { + // annotate_snippets wants the byte index to highlight + let (_, _, start, end) = source.as_bytes().into_iter().fold( + (1, 0, 0, 0), + |(mut line, mut len, mut start, mut end), c| { + len += 1; + if let b'\n' | b'\r' = c { + if line == *begin_line { + end = len; + } + line += 1; + if line == *begin_line { + start = len; + } + } + + (line, len, start, end) + }, + ); + + failure = failure.snippet( + Snippet::source(source) + .line_start(*begin_line) + .origin(filename) + .fold(true) + .annotation(snippets_level.span(start..end)), + ); } - if let Some(witness) = &semver_query.witness { - if witness_generation.show_hints { - let message = config - .handlebars() - .render_template(&witness.hint_template, &pretty_result) - .context("Error instantiating witness hint template.")?; - - config.log_info(|config| { - let note = Style::new() - .fg_color(Some(Color::Ansi(AnsiColor::Cyan))) - .bold(); - writeln!( - config.stdout(), - "{note}note:{note:#} downstream code similar to the following would break:\n\ - {message}\n" - )?; - Ok(()) - })?; - } + if let Some(serde_pretty) = &format_result.serde_pretty { + failure = failure.footer( + Level::Info + .title("lint rule output values") + .snippet(Snippet::source(serde_pretty)), + ); } + + if let Some(witness_hint) = &format_result.witness_hint { + failure = failure.footer( + Level::Note + .title("downstream code similar to the following would break") + .snippet(Snippet::source(&witness_hint)), + ); + } + + message = message.footer(failure); } - Ok(()) + let renderer = Renderer::styled(); + config.log_info(|config| { + writeln!(config.stdout(), "{}", renderer.render(message))?; + Ok(()) + }) } pub(super) fn run_check_release( @@ -351,32 +410,25 @@ pub(super) fn run_check_release( for (semver_query, results) in results_with_errors { required_versions.push(overrides.effective_required_update(semver_query)); - config.log_info(|config| { - writeln!( - config.stdout(), - "\n--- failure {}: {} ---\n", - &semver_query.id, - &semver_query.human_readable_name - )?; - Ok(()) - })?; - print_triggered_lint(config, semver_query, results, witness_generation)?; + print_triggered_lint( + config, + semver_query, + results, + witness_generation, + LintLevel::Deny, + )?; } for (semver_query, results) in results_with_warnings { suggested_versions.push(overrides.effective_required_update(semver_query)); - config.log_info(|config| { - writeln!( - config.stdout(), - "\n--- warning {}: {} ---\n", - semver_query.id, - semver_query.human_readable_name - )?; - Ok(()) - })?; - - print_triggered_lint(config, semver_query, results, witness_generation)?; + print_triggered_lint( + config, + semver_query, + results, + witness_generation, + LintLevel::Warn, + )?; } let required_bump = required_versions.iter().max().copied(); diff --git a/src/lints/function_missing.ron b/src/lints/function_missing.ron index 51d2ce79..2a947209 100644 --- a/src/lints/function_missing.ron +++ b/src/lints/function_missing.ron @@ -45,7 +45,7 @@ SemverQuery( "true": true, }, error_message: "A publicly-visible function cannot be imported by its prior path. A `pub use` may have been removed, or the function itself may have been renamed or removed entirely.", - per_result_error_template: Some("function {{join \"::\" path}}, previously in file {{span_filename}}:{{span_begin_line}}"), + per_result_error_template: Some("function {{join \"::\" path}}"), witness: ( hint_template: r#"{{join "::" path}}(...);"#, ),