feat: Implement sarif format reporter

Fixes #594
This commit is contained in:
Zxilly 2024-07-23 03:19:02 +08:00 committed by Ed Page
parent 32b96444b9
commit 63908449a7
13 changed files with 611 additions and 36 deletions

213
Cargo.lock generated
View file

@ -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"
@ -123,7 +133,7 @@ checksum = "edf3ee19dbc0a46d740f6f0926bde8c50f02bdbc7b536842da28f6ac56513a8b"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.90",
]
[[package]]
@ -208,7 +218,7 @@ dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim 0.11.0",
"strsim",
"terminal_size",
]
@ -221,7 +231,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
"syn 2.0.90",
]
[[package]]
@ -338,9 +348,9 @@ dependencies = [
[[package]]
name = "darling"
version = "0.20.3"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e"
checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
dependencies = [
"darling_core",
"darling_macro",
@ -348,27 +358,58 @@ dependencies = [
[[package]]
name = "darling_core"
version = "0.20.3"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621"
checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim 0.10.0",
"syn",
"strsim",
"syn 2.0.90",
]
[[package]]
name = "darling_macro"
version = "0.20.3"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
dependencies = [
"darling_core",
"quote",
"syn",
"syn 2.0.90",
]
[[package]]
name = "derive_builder"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
dependencies = [
"derive_builder_macro",
]
[[package]]
name = "derive_builder_core"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.90",
]
[[package]]
name = "derive_builder_macro"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
dependencies = [
"derive_builder_core",
"syn 2.0.90",
]
[[package]]
@ -388,7 +429,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.90",
"unicode-xid",
]
@ -401,7 +442,7 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
"syn",
"syn 2.0.90",
]
[[package]]
@ -447,7 +488,7 @@ checksum = "7bdb5411188f7f878a17964798c1264b6b0a9f915bd39b20bf99193c923e1b4e"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.90",
]
[[package]]
@ -500,7 +541,7 @@ checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.90",
]
[[package]]
@ -727,6 +768,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"
@ -857,7 +904,7 @@ dependencies = [
"phf_shared",
"proc-macro2",
"quote",
"syn",
"syn 2.0.90",
"unicase",
]
@ -899,6 +946,16 @@ dependencies = [
"termtree",
]
[[package]]
name = "prettyplease"
version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033"
dependencies = [
"proc-macro2",
"syn 2.0.90",
]
[[package]]
name = "proc-exit"
version = "2.0.2"
@ -1021,6 +1078,12 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "rustversion"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248"
[[package]]
name = "ryu"
version = "1.0.15"
@ -1036,6 +1099,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"
@ -1051,6 +1141,26 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "serde-sarif"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a38c5e5bbaa10cc256774ea394ad62968c31c0e3c3265f65221e02c87dd1a914"
dependencies = [
"anyhow",
"derive_builder",
"prettyplease",
"proc-macro2",
"quote",
"schemafy_lib",
"serde",
"serde_json",
"strum",
"strum_macros",
"syn 2.0.90",
"thiserror",
]
[[package]]
name = "serde_derive"
version = "1.0.215"
@ -1059,7 +1169,7 @@ checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.90",
]
[[package]]
@ -1156,15 +1266,39 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "strsim"
version = "0.10.0"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strsim"
version = "0.11.0"
name = "strum"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
[[package]]
name = "strum_macros"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.90",
]
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
@ -1206,6 +1340,26 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76"
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
]
[[package]]
name = "thread_local"
version = "1.1.8"
@ -1317,6 +1471,7 @@ dependencies = [
"proc-exit",
"regex",
"serde",
"serde-sarif",
"serde_json",
"serde_regex",
"snapbox",
@ -1433,6 +1588,16 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[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"
@ -1702,5 +1867,5 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.90",
]

View file

@ -76,6 +76,7 @@ colorchoice-clap = "1.0.3"
serde_regex = "1.1.0"
regex = "1.10.4"
encoding_rs = "0.8.34"
serde-sarif = "0.5.0"
[dev-dependencies]
assert_fs = "1.1"

View file

@ -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()),
}
}
}

View file

@ -8,6 +8,8 @@ mod report;
use proc_exit::prelude::*;
use typos_cli::report::Report;
fn main() {
human_panic::setup_panic!();
let result = run();
@ -187,6 +189,13 @@ fn run_checks(args: &args::Args) -> proc_exit::ExitResult {
None => None,
};
// HACK: Diff doesn't handle mixing content
let global_reporter = if args.diff {
Box::new(report::PrintSilent)
} else {
args.format.reporter()
};
// Note: file_list and args.path are mutually exclusive, enforced by clap
'path: for path in file_list.as_ref().unwrap_or(&args.path) {
// Note paths are passed through stdin, `-` is treated like a normal path
@ -272,14 +281,8 @@ fn run_checks(args: &args::Args) -> proc_exit::ExitResult {
});
}
// HACK: Diff doesn't handle mixing content
let output_reporter = if args.diff {
Box::new(report::PrintSilent)
} else {
args.format.reporter()
};
let status_reporter = report::MessageStatus::new(output_reporter.as_ref());
let reporter: &dyn typos_cli::report::Report = &status_reporter;
let status_reporter = report::MessageStatus::new(global_reporter.as_ref());
let reporter: &dyn Report = &status_reporter;
let selected_checks: &dyn typos_cli::file::FileChecker = if args.files {
&typos_cli::file::FoundFiles
@ -333,6 +336,11 @@ fn run_checks(args: &args::Args) -> proc_exit::ExitResult {
}
}
if let Err(err) = global_reporter.generate_final_result() {
errors_found = true;
log::error!("could not render end-report: {}", err);
}
if errors_found {
proc_exit::Code::FAILURE.ok()
} else if typos_found {

View file

@ -1,12 +1,15 @@
#![allow(clippy::needless_update)]
use std::io::Write as _;
use std::sync::atomic;
use std::sync::{atomic, Mutex};
use anstream::stdout;
use unicode_width::UnicodeWidthStr;
use serde_sarif::sarif;
use serde_sarif::sarif::{
ArtifactChangeBuilder, ArtifactContentBuilder, FixBuilder, ReplacementBuilder,
};
use typos_cli::report::{Context, Message, Report, Typo};
use unicode_width::UnicodeWidthStr;
const ERROR: anstyle::Style = anstyle::AnsiColor::BrightRed.on_default();
const INFO: anstyle::Style = anstyle::AnsiColor::BrightBlue.on_default();
@ -46,6 +49,10 @@ impl Report for MessageStatus<'_> {
}
self.reporter.report(msg)
}
fn generate_final_result(&self) -> Result<(), std::io::Error> {
self.reporter.generate_final_result()
}
}
#[derive(Debug, Default)]
@ -280,6 +287,230 @@ impl Report for PrintJson {
}
}
#[derive(Debug)]
pub(crate) struct PrintSarif {
results: Mutex<Vec<sarif::Result>>,
error: Mutex<Vec<String>>,
}
impl Default for PrintSarif {
fn default() -> Self {
Self {
results: Mutex::new(Vec::new()),
error: Mutex::new(Vec::new()),
}
}
}
impl Report for PrintSarif {
fn report(&self, msg: Message<'_>) -> Result<(), std::io::Error> {
self.report_sarif(msg).map_err(sarif_error_mapper)
}
fn generate_final_result(&self) -> Result<(), std::io::Error> {
self.generate_final_result().map_err(sarif_error_mapper)
}
}
impl PrintSarif {
fn report_sarif(&self, msg: Message<'_>) -> Result<(), Box<dyn std::error::Error>> {
match &msg {
Message::Typo(msg) => {
if msg.corrections.is_valid() {
return Ok(());
}
let message = type_to_sarif_message(msg).unwrap();
let location = typo_to_sarif_location(msg)?;
let fix =
typo_to_sarif_fix(message.clone(), msg.corrections.clone(), location.clone())?;
let result = typo_to_sarif_result(message, location, fix)?;
self.results.lock().unwrap().push(result);
}
Message::Error(msg) => {
self.error.lock().unwrap().push(msg.msg.clone());
}
Message::BinaryFile(_) => {}
Message::Parse(_) | Message::FileType(_) | Message::File(_) => {}
_ => unimplemented!("New message {:?}", msg),
}
Ok(())
}
fn generate_final_result(&self) -> Result<(), Box<dyn std::error::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(env!("CARGO_PKG_REPOSITORY"))
.build()?,
)
.build()?;
let mut run_builder = sarif::RunBuilder::default();
run_builder
.tool(tool)
.column_kind(sarif::ResultColumnKind::UnicodeCodePoints.to_string())
.results(self.results.lock().unwrap().clone());
if !self.error.lock().unwrap().is_empty() {
let invocations = self
.error
.lock()
.unwrap()
.iter()
.map(|x| {
sarif::InvocationBuilder::default()
.process_start_failure_message(x.clone())
.build()
})
.collect::<Result<Vec<_>, _>>();
if let Err(e) = invocations {
return Err(e.into());
}
run_builder.invocations(invocations.unwrap());
}
let run = run_builder.build()?;
sarif_builder.runs(vec![run]);
let sarif = sarif_builder.build()?;
serde_json::to_writer_pretty(stdout().lock(), &sarif)?;
Ok(())
}
}
fn sarif_error_mapper(error: impl std::fmt::Display) -> std::io::Error {
std::io::Error::new(
std::io::ErrorKind::Other,
format!("failed to generate SARIF output: {error}"),
)
}
fn typo_to_sarif_result(
message: String,
location: sarif::Location,
fix: Option<sarif::Fix>,
) -> Result<sarif::Result, Box<dyn std::error::Error>> {
let mut result = sarif::ResultBuilder::default()
.level(sarif::ResultLevel::Error.to_string())
.message(sarif::MessageBuilder::default().markdown(message).build()?)
.locations(vec![location])
.build()?;
if let Some(fix) = fix {
result.fixes = Some(vec![fix]);
}
Ok(result)
}
fn typo_to_sarif_fix(
message: String,
correct: typos::Status<'_>,
location: sarif::Location,
) -> Result<Option<sarif::Fix>, Box<dyn std::error::Error>> {
let physical_location = location.physical_location.unwrap();
let Some(region) = physical_location.region else {
return Ok(None);
};
let mut replacements = vec![];
match correct {
typos::Status::Corrections(corrections) => {
for correction in corrections.iter() {
replacements.push(
ReplacementBuilder::default()
.deleted_region(region.clone())
.inserted_content(
ArtifactContentBuilder::default()
.text(correction.clone())
.build()
.unwrap(),
)
.build()
.unwrap(),
);
}
}
_ => return Ok(None),
}
let change = ArtifactChangeBuilder::default()
.artifact_location(physical_location.artifact_location.unwrap())
.replacements(replacements)
.build()?;
let fix = FixBuilder::default()
.description(sarif::MessageBuilder::default().markdown(message).build()?)
.artifact_changes(vec![change])
.build()?;
Ok(Some(fix))
}
fn type_to_sarif_message(msg: &Typo<'_>) -> Option<String> {
match &msg.corrections {
typos::Status::Valid => None,
typos::Status::Invalid => Some(format!("`{}` is disallowed", msg.typo)),
typos::Status::Corrections(corrections) => Some(format!(
"`{}` should be {}",
msg.typo,
itertools::join(corrections.iter().map(|s| format!("`{s}`")), ", ",)
)),
}
}
fn typo_to_sarif_location(msg: &Typo<'_>) -> Result<sarif::Location, Box<dyn std::error::Error>> {
let path = match &msg.context {
Some(Context::File(ctx)) => ctx.path,
Some(Context::Path(ctx)) => ctx.path,
None => std::path::Path::new(""),
_ => unimplemented!("New context {:?}", msg),
};
let artifact = sarif::ArtifactLocationBuilder::default()
.uri(
path.display()
.to_string()
.replace(std::path::MAIN_SEPARATOR, "/"),
)
.build()?;
let mut physical = sarif::PhysicalLocationBuilder::default();
physical.artifact_location(artifact);
if let Some(Context::File(context)) = &msg.context {
let start = String::from_utf8_lossy(&msg.buffer[0..msg.byte_offset]);
let column_start = start.chars().count() + 1;
let column_end = msg.typo.chars().count() + column_start;
let line_num = context.line_num;
physical.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()?,
);
}
let location = sarif::LocationBuilder::default()
.physical_location(physical.build()?)
.build()?;
Ok(location)
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -4,6 +4,10 @@ use std::borrow::Cow;
pub trait Report: Send + Sync {
fn report(&self, msg: Message<'_>) -> Result<(), std::io::Error>;
fn generate_final_result(&self) -> Result<(), std::io::Error> {
Ok(())
}
}
#[derive(Clone, Debug, serde::Serialize, derive_more::From)]

View file

@ -45,7 +45,7 @@ Mode:
Output:
--format <FORMAT> Render style for messages [default: long] [possible values: silent, brief,
long, json]
long, json, sarif]
--color <WHEN> Controls when to use color [default: auto] [possible values: auto, always,
never]
-v, --verbose... Increase logging verbosity

View file

@ -0,0 +1,4 @@
[default.extend-words]
invalid = ""
incorrect = "corrected"
different = "size"

View file

@ -0,0 +1,4 @@
Hello good!
Hello invalid!
Hello incorrect!
Hello different size!

View file

@ -0,0 +1 @@
Hello world

View file

@ -0,0 +1,154 @@
bin.name = "typos"
args = "--format sarif --sort"
status.code = 2
stdout = '''
{
"$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json",
"runs": [
{
"columnKind": "unicodeCodePoints",
"results": [
{
"level": "error",
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "./bad"
},
"region": {
"endColumn": 14,
"endLine": 2,
"startColumn": 7,
"startLine": 2
}
}
}
],
"message": {
"markdown": "`invalid` is disallowed"
}
},
{
"fixes": [
{
"artifactChanges": [
{
"artifactLocation": {
"uri": "./bad"
},
"replacements": [
{
"deletedRegion": {
"endColumn": 16,
"endLine": 3,
"startColumn": 7,
"startLine": 3
},
"insertedContent": {
"text": "corrected"
}
}
]
}
],
"description": {
"markdown": "`incorrect` should be `corrected`"
}
}
],
"level": "error",
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "./bad"
},
"region": {
"endColumn": 16,
"endLine": 3,
"startColumn": 7,
"startLine": 3
}
}
}
],
"message": {
"markdown": "`incorrect` should be `corrected`"
}
},
{
"fixes": [
{
"artifactChanges": [
{
"artifactLocation": {
"uri": "./bad"
},
"replacements": [
{
"deletedRegion": {
"endColumn": 16,
"endLine": 4,
"startColumn": 7,
"startLine": 4
},
"insertedContent": {
"text": "size"
}
}
]
}
],
"description": {
"markdown": "`different` should be `size`"
}
}
],
"level": "error",
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "./bad"
},
"region": {
"endColumn": 16,
"endLine": 4,
"startColumn": 7,
"startLine": 4
}
}
}
],
"message": {
"markdown": "`different` should be `size`"
}
},
{
"level": "error",
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "./some-incorrect-file"
}
}
}
],
"message": {
"markdown": "`incorrect` should be `corrected`"
}
}
],
"tool": {
"driver": {
"informationUri": "https://github.com/crate-ci/typos",
"name": "typos"
}
}
}
],
"version": "2.1.0"
}'''
stderr = ""

View file

@ -87,6 +87,7 @@ allow = [
"MIT",
"MIT-0",
"Apache-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"MPL-2.0",
"Unicode-DFS-2016",