diff --git a/Cargo.lock b/Cargo.lock index eb9318c06..dce8a2d9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + [[package]] name = "addr2line" version = "0.21.0" @@ -218,7 +228,7 @@ version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.66", @@ -345,14 +355,38 @@ dependencies = [ "memchr", ] +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core 0.14.4", + "darling_macro 0.14.4", +] + [[package]] name = "darling" version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.3", + "darling_macro 0.20.3", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn 1.0.109", ] [[package]] @@ -369,17 +403,59 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core 0.14.4", + "quote", + "syn 1.0.109", +] + [[package]] name = "darling_macro" version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ - "darling_core", + "darling_core 0.20.3", "quote", "syn 2.0.66", ] +[[package]] +name = "derive_builder" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" +dependencies = [ + "darling 0.14.4", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder_macro" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" +dependencies = [ + "derive_builder_core", + "syn 1.0.109", +] + [[package]] name = "derive_more" version = "0.99.17" @@ -399,7 +475,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e8ef033054e131169b8f0f9a7af8f5533a9436fadf3c500ed547f730f07090d" dependencies = [ - "darling", + "darling 0.20.3", "proc-macro2", "quote", "syn 2.0.66", @@ -619,6 +695,12 @@ version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -729,6 +811,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.149" @@ -913,6 +1001,16 @@ dependencies = [ "termtree", ] +[[package]] +name = "prettyplease" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" +dependencies = [ + "proc-macro2", + "syn 2.0.66", +] + [[package]] name = "proc-exit" version = "2.0.1" @@ -1044,6 +1142,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + [[package]] name = "ryu" version = "1.0.15" @@ -1059,6 +1163,33 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schemafy_core" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bec29dddcfe60f92f3c0d422707b8b56473983ef0481df8d5236ed3ab8fdf24" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "schemafy_lib" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3d87f1df246a9b7e2bfd1f4ee5f88e48b11ef9cfc62e63f0dead255b1a6f5f" +dependencies = [ + "Inflector", + "proc-macro2", + "quote", + "schemafy_core", + "serde", + "serde_derive", + "serde_json", + "syn 1.0.109", + "uriparse", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1080,6 +1211,26 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-sarif" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d878dc2c454b118932f9e8aef2a228ec99dcac000d6204a0061d9b2ef322304" +dependencies = [ + "anyhow", + "derive_builder", + "prettyplease", + "proc-macro2", + "quote", + "schemafy_lib", + "serde", + "serde_json", + "strum", + "strum_macros", + "syn 2.0.66", + "thiserror", +] + [[package]] name = "serde_derive" version = "1.0.203" @@ -1194,6 +1345,25 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 1.0.109", +] + [[package]] name = "syn" version = "1.0.109" @@ -1378,6 +1548,7 @@ dependencies = [ "proc-exit", "regex", "serde", + "serde-sarif", "serde_json", "serde_regex", "snapbox", @@ -1498,6 +1669,16 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +[[package]] +name = "uriparse" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0200d0fc04d809396c2ad43f3c95da3582a2556eba8d453c1087f4120ee352ff" +dependencies = [ + "fnv", + "lazy_static", +] + [[package]] name = "utf8parse" version = "0.2.1" diff --git a/crates/typos-cli/Cargo.toml b/crates/typos-cli/Cargo.toml index 2fa4c47ad..f6f67c408 100644 --- a/crates/typos-cli/Cargo.toml +++ b/crates/typos-cli/Cargo.toml @@ -77,6 +77,7 @@ colorchoice-clap = "1.0.3" serde_regex = "1.1.0" regex = "1.10.4" encoding_rs = "0.8.34" +serde-sarif = "0.4.2" [dev-dependencies] assert_fs = "1.1" diff --git a/crates/typos-cli/src/bin/typos-cli/args.rs b/crates/typos-cli/src/bin/typos-cli/args.rs index 3541533cf..a1a7f02d9 100644 --- a/crates/typos-cli/src/bin/typos-cli/args.rs +++ b/crates/typos-cli/src/bin/typos-cli/args.rs @@ -10,6 +10,7 @@ pub(crate) enum Format { #[default] Long, Json, + Sarif, } impl Format { @@ -19,6 +20,7 @@ impl Format { Format::Brief => Box::new(crate::report::PrintBrief), Format::Long => Box::new(crate::report::PrintLong), Format::Json => Box::new(crate::report::PrintJson), + Format::Sarif => Box::new(crate::report::PrintSarif::default()), } } } diff --git a/crates/typos-cli/src/bin/typos-cli/main.rs b/crates/typos-cli/src/bin/typos-cli/main.rs index a63af8046..1a34dc24b 100644 --- a/crates/typos-cli/src/bin/typos-cli/main.rs +++ b/crates/typos-cli/src/bin/typos-cli/main.rs @@ -1,13 +1,14 @@ -use std::io::{BufRead as _, BufReader, Write as _}; +use std::io::{BufRead as _, BufReader, Error, Write as _}; use std::path::PathBuf; use clap::Parser; +use proc_exit::prelude::*; + +use typos_cli::report::Report; mod args; mod report; -use proc_exit::prelude::*; - fn main() { human_panic::setup_panic!(); let result = run(); @@ -316,6 +317,13 @@ fn run_checks(args: &args::Args) -> proc_exit::ExitResult { if status_reporter.errors_found() { errors_found = true; } + + match status_reporter.finalize() { + Ok(_) => {} + Err(err) => { + log::error!("Error finalizing: {}", err); + } + } } if errors_found { diff --git a/crates/typos-cli/src/bin/typos-cli/report.rs b/crates/typos-cli/src/bin/typos-cli/report.rs index 45557226b..5bf3af3c5 100644 --- a/crates/typos-cli/src/bin/typos-cli/report.rs +++ b/crates/typos-cli/src/bin/typos-cli/report.rs @@ -1,11 +1,13 @@ #![allow(clippy::needless_update)] -use std::io::Write as _; -use std::sync::atomic; +use std::cell::RefCell; +use std::io::{Write as _}; +use std::sync::{atomic, Mutex}; use anstream::stdout; +use serde_sarif::sarif; +use serde_sarif::sarif::Invocation; use unicode_width::UnicodeWidthStr; - use typos_cli::report::{Context, Message, Report, Typo}; const ERROR: anstyle::Style = anstyle::AnsiColor::BrightRed.on_default(); @@ -46,6 +48,10 @@ impl<'r> Report for MessageStatus<'r> { } self.reporter.report(msg) } + + fn finalize(&self) -> Result<(), std::io::Error> { + self.reporter.finalize() + } } #[derive(Debug, Default)] @@ -280,6 +286,153 @@ impl Report for PrintJson { } } +#[derive(Debug)] +pub(crate) struct PrintSarif { + results: Mutex>>, + error: Mutex>>, +} + +impl Default for PrintSarif { + fn default() -> Self { + Self { + results: Mutex::new(RefCell::new(Vec::new())), + error: Mutex::new(RefCell::new(Vec::new())), + } + } +} + +impl Report for PrintSarif { + fn report(&self, msg: Message<'_>) -> Result<(), std::io::Error> { + match &msg { + Message::Typo(msg) => { + let start = String::from_utf8_lossy(&msg.buffer[0..msg.byte_offset]); + let column_start = + unicode_segmentation::UnicodeSegmentation::graphemes(start.as_ref(), true).count() + 1; + let column_end = + unicode_segmentation::UnicodeSegmentation::graphemes(msg.typo, true).count() + column_start; + let line_num = msg.context.as_ref().map(|context| match context { + Context::File(context) => context.line_num, + _ => 1, + }).unwrap_or(1); + + // we have nothing to print here + if msg.corrections == typos::Status::Valid { + return Ok(()); + } + + let message = match &msg.corrections { + typos::Status::Invalid => { + format!("`{}` is disallowed", msg.typo) + } + typos::Status::Corrections(corrections) => { + format!("`{}` should be {}", msg.typo, itertools::join( + corrections.iter().map(|s| format!("`{}`", s)), + ", ", + )) + } + typos::Status::Valid => { unreachable!("has been checked earlier") } + }; + + let location = sarif::LocationBuilder::default() + .physical_location(sarif::PhysicalLocationBuilder::default() + .artifact_location( + sarif::ArtifactLocationBuilder::default() + .uri(msg.context.as_ref().map(|context| match context { + Context::File(context) => context.path.to_str().unwrap_or(""), + _ => "", + }).unwrap_or("")) + .build().map_err(error_to_io_error)? + ) + .region( + sarif::RegionBuilder::default() + .start_line(line_num as i64) + .end_line(line_num as i64) + .start_column(column_start as i64) + .end_column(column_end as i64) + .build().map_err(error_to_io_error)? + ) + .build().map_err(error_to_io_error)?) + .build().map_err(error_to_io_error)?; + + + let result = sarif::ResultBuilder::default() + .level(sarif::ResultLevel::Error.to_string()) + .message(sarif::MessageBuilder::default() + .markdown(message) + .build().map_err(error_to_io_error)?) + .locations(vec![location]) + .build().map_err(error_to_io_error)?; + + self.results.lock().unwrap().borrow_mut().push(result); + } + Message::Parse(msg) => { + self.error.lock().unwrap().borrow_mut().push(msg.data.to_owned()); + } + Message::Error(msg) => { + self.error.lock().unwrap().borrow_mut().push(msg.msg.clone()); + } + Message::BinaryFile(_) => {} + Message::FileType(_) => {} + Message::File(_) => {} + _ => unimplemented!("New message {:?}", msg), + } + + Ok(()) + } + + fn finalize(&self) -> Result<(), std::io::Error> { + let mut sarif_builder = sarif::SarifBuilder::default(); + sarif_builder.version(sarif::Version::V2_1_0.to_string()) + .schema(sarif::SCHEMA_URL); + + + let tool = sarif::ToolBuilder::default() + .driver(sarif::ToolComponentBuilder::default() + .name("typos") + .information_uri("https://github.com/crate-ci/typos") + .build().map_err(error_to_io_error)?) + .build().map_err(error_to_io_error)?; + + let mut run_builder = sarif::RunBuilder::default(); + run_builder.tool(tool) + .results(self.results.lock().unwrap().borrow().clone()); + + + if self.error.lock().unwrap().borrow().len() > 0 { + let invocations: Vec> = self.error.lock().unwrap().borrow().iter().map( + |x| { + sarif::InvocationBuilder::default() + .process_start_failure_message(x.to_owned().clone()) + .build().map_err(error_to_io_error) + } + ).collect(); + + let error = invocations.iter().find(|x| x.is_err()); + // if invocations contains any error, return error + if error.is_some() { + let error = error.unwrap().as_ref().unwrap_err(); + return Err(std::io::Error::new(error.kind(), error.to_string())); + } + + let invocations: Vec = invocations.into_iter().map(|x| x.unwrap()).collect(); + + run_builder.invocations( + invocations + ); + } + + let sarif = sarif_builder.build().map_err(error_to_io_error)?; + + write!(stdout().lock(), "{}", serde_json::to_string_pretty(&sarif)?)?; + + Ok(()) + } +} + +fn error_to_io_error(error: impl std::fmt::Display) -> std::io::Error { + std::io::Error::new(std::io::ErrorKind::Other, error.to_string()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/typos-cli/src/report.rs b/crates/typos-cli/src/report.rs index c6f50fa0f..68a09bd5e 100644 --- a/crates/typos-cli/src/report.rs +++ b/crates/typos-cli/src/report.rs @@ -4,6 +4,10 @@ use std::borrow::Cow; pub trait Report: Send + Sync { fn report(&self, msg: Message<'_>) -> Result<(), std::io::Error>; + + fn finalize(&self) -> Result<(), std::io::Error> { + Ok(()) + } } #[derive(Clone, Debug, serde::Serialize, derive_more::From)] diff --git a/crates/typos-cli/tests/cmd/help.toml b/crates/typos-cli/tests/cmd/help.toml index 772f09f48..c02facdbb 100644 --- a/crates/typos-cli/tests/cmd/help.toml +++ b/crates/typos-cli/tests/cmd/help.toml @@ -45,7 +45,7 @@ Mode: Output: --format Render style for messages [default: long] [possible values: silent, brief, - long, json] + long, json, sarif] --color Controls when to use color [default: auto] [possible values: auto, always, never] -v, --verbose... Increase logging verbosity