diff --git a/sarif-fmt/src/bin.rs b/sarif-fmt/src/bin.rs index b4fd7703..3ef7caa5 100644 --- a/sarif-fmt/src/bin.rs +++ b/sarif-fmt/src/bin.rs @@ -145,162 +145,162 @@ use std::str::FromStr; use std::usize; fn process(mut reader: R) -> Result { - let mut data = String::new(); - reader.read_to_string(&mut data)?; - let s: sarif::Sarif = serde_json::from_str(&data)?; - Ok(s) + let mut data = String::new(); + reader.read_to_string(&mut data)?; + let s: sarif::Sarif = serde_json::from_str(&data)?; + Ok(s) } fn try_find_file( - physical_location: &sarif::PhysicalLocation, - run: &sarif::Run, + physical_location: &sarif::PhysicalLocation, + run: &sarif::Run, ) -> Result { - let artifact_location = physical_location - .artifact_location - .as_ref() - .map_or_else(|| Err(anyhow::anyhow!("No artifact location.")), Ok)?; - let uri = artifact_location - .uri - .as_ref() - .map_or_else(|| Err(anyhow::anyhow!("No uri.")), Ok)?; - let path = Path::new(uri); - - // if the path is absolute and exists, return it, else - if path.is_absolute() { - if path.exists() { - return Ok(path.to_path_buf()); - } - return Err(anyhow::anyhow!("Path does not exist")); + let artifact_location = physical_location + .artifact_location + .as_ref() + .map_or_else(|| Err(anyhow::anyhow!("No artifact location.")), Ok)?; + let uri = artifact_location + .uri + .as_ref() + .map_or_else(|| Err(anyhow::anyhow!("No uri.")), Ok)?; + let path = Path::new(uri); + + // if the path is absolute and exists, return it, else + if path.is_absolute() { + if path.exists() { + return Ok(path.to_path_buf()); } - - // if it's a relative path, it's more complicated and dictated by the SARIF spec - - // 1. check if uri base id exists -- it SHOULD exist, but may not - if let Some(uri_base_id) = artifact_location.uri_base_id.as_ref() { - // check if this is defined in originalUriBaseIds - if let Some(original_uri_base_ids) = run.original_uri_base_ids.as_ref() { - let mut path = PathBuf::new(); - if let Some(uri_base) = original_uri_base_ids.get(uri_base_id) { - // todo: this doesn't handle recursive uri_base_id... - if let Some(uri) = uri_base.uri.as_ref() { - path.push(uri); - } - } - path.push(uri); - if path.exists() { - return Ok(path); - } - } - - // just check if the relative path exists by chance - if path.exists() { - return Ok(path.to_path_buf()); + return Err(anyhow::anyhow!("Path does not exist")); + } + + // if it's a relative path, it's more complicated and dictated by the SARIF spec + + // 1. check if uri base id exists -- it SHOULD exist, but may not + if let Some(uri_base_id) = artifact_location.uri_base_id.as_ref() { + // check if this is defined in originalUriBaseIds + if let Some(original_uri_base_ids) = run.original_uri_base_ids.as_ref() { + let mut path = PathBuf::new(); + if let Some(uri_base) = original_uri_base_ids.get(uri_base_id) { + // todo: this doesn't handle recursive uri_base_id... + if let Some(uri) = uri_base.uri.as_ref() { + path.push(uri); } + } + path.push(uri); + if path.exists() { + return Ok(path); + } } + + // just check if the relative path exists by chance if path.exists() { - Ok(path.to_path_buf()) - } else { - Err(anyhow::anyhow!("Path not found: {:#?}", path)) + return Ok(path.to_path_buf()); } + } + if path.exists() { + Ok(path.to_path_buf()) + } else { + Err(anyhow::anyhow!("Path not found: {:#?}", path)) + } } fn get_physical_location_contents( - physical_location: &sarif::PhysicalLocation, - run: &sarif::Run, + physical_location: &sarif::PhysicalLocation, + run: &sarif::Run, ) -> Result { - let path = try_find_file(physical_location, run)?; - let mut file = File::open(path)?; - let mut contents = String::new(); - file.read_to_string(&mut contents)?; - Ok(contents) + let path = try_find_file(physical_location, run)?; + let mut file = File::open(path)?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + Ok(contents) } fn try_get_byte_offset( - file_id: usize, - files: &SimpleFiles<&String, String>, - row: i64, - column: i64, + file_id: usize, + files: &SimpleFiles<&String, String>, + row: i64, + column: i64, ) -> Result { - files - .line_range(file_id, row as usize - 1)? - .find(|byte| { - if let Ok(location) = files.location(file_id, *byte) { - location.column_number == column as usize - } else { - false - } - }) - .ok_or_else(|| anyhow::anyhow!("Byte offset not found")) + files + .line_range(file_id, row as usize - 1)? + .find(|byte| { + if let Ok(location) = files.location(file_id, *byte) { + location.column_number == column as usize + } else { + false + } + }) + .ok_or_else(|| anyhow::anyhow!("Byte offset not found")) } fn get_byte_range( - file_id: usize, - files: &SimpleFiles<&String, String>, - region: &sarif::Region, + file_id: usize, + files: &SimpleFiles<&String, String>, + region: &sarif::Region, ) -> (Option, Option) { - // todo: support character regions - let byte_offset = if let Some(byte_offset) = region.byte_offset { - Some(byte_offset as usize) - } else if let (Some(start_line), Some(start_column)) = - (region.start_line, region.start_column.or(Some(1))) + // todo: support character regions + let byte_offset = if let Some(byte_offset) = region.byte_offset { + Some(byte_offset as usize) + } else if let (Some(start_line), Some(start_column)) = + (region.start_line, region.start_column.or(Some(1))) + { + if let Ok(byte_offset) = + try_get_byte_offset(file_id, files, start_line, start_column) { - if let Ok(byte_offset) = - try_get_byte_offset(file_id, files, start_line, start_column) - { - Some(byte_offset) - } else { - None - } + Some(byte_offset) } else { - None - }; - - let byte_end = if let Some(byte_offset) = byte_offset { - if let Some(byte_length) = region.byte_length { - Some(byte_offset + byte_length as usize) - } else if let (Some(end_line), Some(end_column)) = ( - region.end_line.map_or_else(|| region.start_line, Some), // if no end_line, default to start_line - region.end_column.map_or_else( - // if no end column use the line's last column - || { - region - .end_line - .map_or_else(|| region.start_line, Some) - .and_then(|start_line| { - files - .line_range(file_id, start_line as usize - 1) - .map_or(None, Option::from) - .and_then(|byte_range| { - byte_range.last().and_then(|last_byte| { - files - .column_number( - file_id, - start_line as usize - 1, - last_byte, - ) - .map_or(None, |v| Option::from(v as i64)) - }) - }) - }) - }, - Some, - ), - ) { - if let Ok(byte_offset) = - try_get_byte_offset(file_id, files, end_line, end_column) - { - Some(byte_offset) - } else { - Some(byte_offset) - } - } else { - Some(byte_offset) - } + None + } + } else { + None + }; + + let byte_end = if let Some(byte_offset) = byte_offset { + if let Some(byte_length) = region.byte_length { + Some(byte_offset + byte_length as usize) + } else if let (Some(end_line), Some(end_column)) = ( + region.end_line.map_or_else(|| region.start_line, Some), // if no end_line, default to start_line + region.end_column.map_or_else( + // if no end column use the line's last column + || { + region + .end_line + .map_or_else(|| region.start_line, Some) + .and_then(|start_line| { + files + .line_range(file_id, start_line as usize - 1) + .map_or(None, Option::from) + .and_then(|byte_range| { + byte_range.last().and_then(|last_byte| { + files + .column_number( + file_id, + start_line as usize - 1, + last_byte, + ) + .map_or(None, |v| Option::from(v as i64)) + }) + }) + }) + }, + Some, + ), + ) { + if let Ok(byte_offset) = + try_get_byte_offset(file_id, files, end_line, end_column) + { + Some(byte_offset) + } else { + Some(byte_offset) + } } else { - None - }; + Some(byte_offset) + } + } else { + None + }; - (byte_offset, byte_end) + (byte_offset, byte_end) } // If kind (§3.27.9) has any value other than "fail", then if level is absent, it SHALL default to "none", and if it is present, it SHALL have the value "none". @@ -323,98 +323,98 @@ fn get_byte_range( // IF level has not yet been set THEN // SET level to "warning". fn resolve_level( - rules: &[sarif::ReportingDescriptor], - run: &sarif::Run, - result: &sarif::Result, + rules: &[sarif::ReportingDescriptor], + run: &sarif::Run, + result: &sarif::Result, ) -> sarif::ResultLevel { - result - .kind - .as_ref() - // 3.27.9 kind property - // If kind is absent, it SHALL default to "fail". - .map_or(Some("fail"), |k| k.as_str()) - .and_then(|kind| match kind { - // If kind has the value "fail" and level is absent, then level SHALL be determined by the following procedure: - "fail" => match result.level.as_ref() { - Some(level) => level.as_str().and_then(|level| { - sarif::ResultLevel::from_str(level).map_or(None, Option::from) - }), - None => result.rule.as_ref().and_then(|rule| { - // IF rule (§3.27.7) is present THEN - rule.index.and_then(|rule_index| { - rules - .get(rule_index as usize) - // LET theDescriptor be the reportingDescriptor object (§3.49) that it specifies. - // # Is there a configuration override for the level property? - .and_then(|the_descriptor| { - // IF result.provenance.invocationIndex (§3.27.29, §3.48.6) is >= 0 THEN - result - .provenance + result + .kind + .as_ref() + // 3.27.9 kind property + // If kind is absent, it SHALL default to "fail". + .map_or(Some("fail"), |k| k.as_str()) + .and_then(|kind| match kind { + // If kind has the value "fail" and level is absent, then level SHALL be determined by the following procedure: + "fail" => match result.level.as_ref() { + Some(level) => level.as_str().and_then(|level| { + sarif::ResultLevel::from_str(level).map_or(None, Option::from) + }), + None => result.rule.as_ref().and_then(|rule| { + // IF rule (§3.27.7) is present THEN + rule.index.and_then(|rule_index| { + rules + .get(rule_index as usize) + // LET theDescriptor be the reportingDescriptor object (§3.49) that it specifies. + // # Is there a configuration override for the level property? + .and_then(|the_descriptor| { + // IF result.provenance.invocationIndex (§3.27.29, §3.48.6) is >= 0 THEN + result + .provenance + .as_ref() + .and_then(|provenance| { + provenance.invocation_index.and_then(|invocation_index| { + run + .invocations + .iter() + .flatten() + .collect::>() + .get(invocation_index as usize) + // LET theInvocation be the invocation object (§3.20) that it specifies. + // IF theInvocation.ruleConfigurationOverrides (§3.20.5) is present + // AND it contains a configurationOverride object (§3.51) whose + // descriptor property (§3.51.2) specifies theDescriptor THEN + .and_then(|the_invocation| { + the_invocation + .rule_configuration_overrides + .as_ref() + .and_then(|rule_configuration_overrides| { + rule_configuration_overrides + .iter() + .find(|v| { + v.descriptor.id.as_ref() + == Some(&the_descriptor.id) + }) + .and_then(|the_override| { + the_override + .configuration + .level .as_ref() - .and_then(|provenance| { - provenance.invocation_index.and_then(|invocation_index| { - run - .invocations - .iter() - .flatten() - .collect::>() - .get(invocation_index as usize) - // LET theInvocation be the invocation object (§3.20) that it specifies. - // IF theInvocation.ruleConfigurationOverrides (§3.20.5) is present - // AND it contains a configurationOverride object (§3.51) whose - // descriptor property (§3.51.2) specifies theDescriptor THEN - .and_then(|the_invocation| { - the_invocation - .rule_configuration_overrides - .as_ref() - .and_then(|rule_configuration_overrides| { - rule_configuration_overrides - .iter() - .find(|v| { - v.descriptor.id.as_ref() - == Some(&the_descriptor.id) - }) - .and_then(|the_override| { - the_override - .configuration - .level - .as_ref() - .and_then(|value| { - value.as_str().and_then(|level| { - sarif::ResultLevel::from_str(level) - .map_or(None, Option::from) - }) - }) - }) - }) - }) - }) - }) - .or_else(|| { - // # There is no configuration override for level. Is there a default configuration for it? - // IF theDescriptor.defaultConfiguration.level (§3.49.14, §, §3.50.3) is present THEN - // SET level to theDescriptor.defaultConfiguration.level. - the_descriptor.default_configuration.as_ref().and_then( - |default_configuration| { - default_configuration.level.as_ref().and_then(|value| { - value.as_str().and_then(|level| { - sarif::ResultLevel::from_str(level) - .map_or(None, Option::from) - }) - }) - }, - ) + .and_then(|value| { + value.as_str().and_then(|level| { + sarif::ResultLevel::from_str(level) + .map_or(None, Option::from) + }) }) + }) }) + }) }) - }), - }, - // If kind (§3.27.9) has any value other than "fail", then if level is absent, it SHALL default to "none", and if it is present, it SHALL have the value "none". - _ => Some(sarif::ResultLevel::None), - }) - // IF level has not yet been set THEN - // SET level to "warning". - .unwrap_or(sarif::ResultLevel::Warning) + }) + .or_else(|| { + // # There is no configuration override for level. Is there a default configuration for it? + // IF theDescriptor.defaultConfiguration.level (§3.49.14, §, §3.50.3) is present THEN + // SET level to theDescriptor.defaultConfiguration.level. + the_descriptor.default_configuration.as_ref().and_then( + |default_configuration| { + default_configuration.level.as_ref().and_then(|value| { + value.as_str().and_then(|level| { + sarif::ResultLevel::from_str(level) + .map_or(None, Option::from) + }) + }) + }, + ) + }) + }) + }) + }), + }, + // If kind (§3.27.9) has any value other than "fail", then if level is absent, it SHALL default to "none", and if it is present, it SHALL have the value "none". + _ => Some(sarif::ResultLevel::None), + }) + // IF level has not yet been set THEN + // SET level to "warning". + .unwrap_or(sarif::ResultLevel::Warning) } // IF theMessage.text is present and the desired language is theRun.language THEN @@ -437,400 +437,388 @@ fn resolve_level( // IF the string has not yet been found THEN // The lookup procedure fails (which means the SARIF log file is invalid). fn resolve_message_text_from_result( - result: &sarif::Result, - run: &sarif::Run, + result: &sarif::Result, + run: &sarif::Run, ) -> Option { - result - .message - .text - .as_ref() - .cloned() - // IF the string has not yet been found THEN - .or_else(|| { - // IF theMessage occurs as the value of result.message (§3.27.11) THEN - result.rule.as_ref().and_then(|the_rule| { - the_rule.index.and_then(|rule_index| { - run.tool.driver.rules.as_ref().and_then(|rules| { - // LET theRule be the reportingDescriptor object (§3.49), an element of theComponent.rules (§3.19.23), which defines the rule that was violated by this result. - // IF theRule exists AND theRule.messageStrings (§3.49.11) is present AND contains a property whose name equals theMessage.id THEN - rules.get(rule_index as usize).and_then(|the_rule| { - the_rule - .message_strings - .as_ref() - .and_then(|message_strings| { - result.message.id.as_ref().and_then(|message_id| { - // LET theMFMS be the multiformatMessageString object (§3.12) that is the value of that property. - // Use the text or markdown property of theMFMS as appropriate. - message_strings - .get(message_id) - .map(|the_mfms| the_mfms.text.clone()) - }) - }) - }) - }) + result + .message + .text + .as_ref() + .cloned() + // IF the string has not yet been found THEN + .or_else(|| { + // IF theMessage occurs as the value of result.message (§3.27.11) THEN + result.rule.as_ref().and_then(|the_rule| { + the_rule.index.and_then(|rule_index| { + run.tool.driver.rules.as_ref().and_then(|rules| { + // LET theRule be the reportingDescriptor object (§3.49), an element of theComponent.rules (§3.19.23), which defines the rule that was violated by this result. + // IF theRule exists AND theRule.messageStrings (§3.49.11) is present AND contains a property whose name equals theMessage.id THEN + rules.get(rule_index as usize).and_then(|the_rule| { + the_rule + .message_strings + .as_ref() + .and_then(|message_strings| { + result.message.id.as_ref().and_then(|message_id| { + // LET theMFMS be the multiformatMessageString object (§3.12) that is the value of that property. + // Use the text or markdown property of theMFMS as appropriate. + message_strings + .get(message_id) + .map(|the_mfms| the_mfms.text.clone()) + }) }) }) + }) }) - // IF the string has not yet been found THEN - // IF theComponent.globalMessageStrings (§3.19.22) is present AND contains a property whose name equals theMessage.id THEN - // LET theMFMS be the multiformatMessageString object that is the value of that property. - // Use the text or markdown property of theMFMS as appropriate. - .or_else(|| { - run.tool.driver.global_message_strings.as_ref().and_then( - |global_message_strings| { - result.message.id.as_ref().and_then(|message_id| { - global_message_strings - .get(message_id) - .map(|the_mfms| the_mfms.text.clone()) - }) - }, - ) - }) + }) + }) // IF the string has not yet been found THEN - // The lookup procedure fails (which means the SARIF log file is invalid). - // .or_else(|| None) # uncesscary but written for illustration + // IF theComponent.globalMessageStrings (§3.19.22) is present AND contains a property whose name equals theMessage.id THEN + // LET theMFMS be the multiformatMessageString object that is the value of that property. + // Use the text or markdown property of theMFMS as appropriate. + .or_else(|| { + run.tool.driver.global_message_strings.as_ref().and_then( + |global_message_strings| { + result.message.id.as_ref().and_then(|message_id| { + global_message_strings + .get(message_id) + .map(|the_mfms| the_mfms.text.clone()) + }) + }, + ) + }) + // IF the string has not yet been found THEN + // The lookup procedure fails (which means the SARIF log file is invalid). + // .or_else(|| None) # uncesscary but written for illustration } fn resolve_full_description_from_result( - rules: &[sarif::ReportingDescriptor], - result: &sarif::Result, + rules: &[sarif::ReportingDescriptor], + result: &sarif::Result, ) -> Option { - result - .rule_index - .and_then(|rule_index| { - rules.get(rule_index as usize).and_then(|the_descriptor| { - the_descriptor - .full_description - .as_ref() - .map(|mfms| mfms.text.clone()) - }) - }) - .or_else(|| { - result.rule.as_ref().and_then(|rule| { - rule.index.and_then(|rule_index| { - rules.get(rule_index as usize).and_then(|the_descriptor| { - the_descriptor - .full_description - .as_ref() - .map(|mfms| mfms.text.clone()) - }) - }) - }) + result + .rule_index + .and_then(|rule_index| { + rules.get(rule_index as usize).and_then(|the_descriptor| { + the_descriptor + .full_description + .as_ref() + .map(|mfms| mfms.text.clone()) + }) + }) + .or_else(|| { + result.rule.as_ref().and_then(|rule| { + rule.index.and_then(|rule_index| { + rules.get(rule_index as usize).and_then(|the_descriptor| { + the_descriptor + .full_description + .as_ref() + .map(|mfms| mfms.text.clone()) + }) }) + }) + }) } fn resolve_short_description_from_result( - rules: &[sarif::ReportingDescriptor], - result: &sarif::Result, + rules: &[sarif::ReportingDescriptor], + result: &sarif::Result, ) -> Option { - result - .rule_index - .and_then(|rule_index| { - rules.get(rule_index as usize).and_then(|the_descriptor| { - the_descriptor - .short_description - .as_ref() - .map(|mfms| mfms.text.clone()) - }) - }) - .or_else(|| { - result.rule.as_ref().and_then(|rule| { - rule.index.and_then(|rule_index| { - rules.get(rule_index as usize).and_then(|the_descriptor| { - the_descriptor - .short_description - .as_ref() - .map(|mfms| mfms.text.clone()) - }) - }) - }) + result + .rule_index + .and_then(|rule_index| { + rules.get(rule_index as usize).and_then(|the_descriptor| { + the_descriptor + .short_description + .as_ref() + .map(|mfms| mfms.text.clone()) + }) + }) + .or_else(|| { + result.rule.as_ref().and_then(|rule| { + rule.index.and_then(|rule_index| { + rules.get(rule_index as usize).and_then(|the_descriptor| { + the_descriptor + .short_description + .as_ref() + .map(|mfms| mfms.text.clone()) + }) }) + }) + }) } fn to_writer_plain(sarif: &sarif::Sarif) -> Result<()> { - let mut files = SimpleFiles::new(); - sarif.runs.iter().try_for_each(|run| -> Result<()> { - let mut diagnostics = vec![]; - if let Some(results) = run.results.as_ref() { - results.iter().try_for_each(|result| -> Result<()> { - let rules = vec![]; - let rules = run.tool.driver.rules.as_ref().unwrap_or(&rules); - let level = resolve_level(rules, run, result); - - if let (Some(text), Some(locations)) = ( - resolve_message_text_from_result(result, run), - result.locations.as_ref(), - ) { - locations.iter().for_each(|location| { - if let Some((file_id, range)) = location - .physical_location - .as_ref() - .and_then(|physical_location| { - physical_location.artifact_location.as_ref().and_then( - |artifact_location| { - artifact_location.uri.as_ref().and_then(|uri| { - physical_location.region.as_ref().and_then(|region| { - get_physical_location_contents(physical_location, run) - .ok() - .and_then(|contents| { - let file_id = files.add(uri, contents); - if let (Some(range_start), Some(range_end)) = - get_byte_range(file_id, &files, region) - { - Some((file_id, range_start..range_end)) - } else { - None - } - }) - }) - }) - }, - ) - }) - { - if let (Ok(name), Ok(location)) = - (files.name(file_id), files.location(file_id, range.start)) + let mut files = SimpleFiles::new(); + sarif.runs.iter().try_for_each(|run| -> Result<()> { + let mut diagnostics = vec![]; + if let Some(results) = run.results.as_ref() { + results.iter().try_for_each(|result| -> Result<()> { + let rules = vec![]; + let rules = run.tool.driver.rules.as_ref().unwrap_or(&rules); + let level = resolve_level(rules, run, result); + + if let (Some(text), Some(locations)) = ( + resolve_message_text_from_result(result, run), + result.locations.as_ref(), + ) { + locations.iter().for_each(|location| { + if let Some((file_id, range)) = location + .physical_location + .as_ref() + .and_then(|physical_location| { + physical_location.artifact_location.as_ref().and_then( + |artifact_location| { + artifact_location.uri.as_ref().and_then(|uri| { + physical_location.region.as_ref().and_then(|region| { + get_physical_location_contents(physical_location, run) + .ok() + .and_then(|contents| { + let file_id = files.add(uri, contents); + if let (Some(range_start), Some(range_end)) = + get_byte_range(file_id, &files, region) { - let diagnostic = ( - name.clone(), - level, - location.line_number, - location.column_number, - text.clone(), - ); - diagnostics.push(diagnostic); + Some((file_id, range_start..range_end)) } else { - // todo: no location found + None } - } - }); - // todo: no location found - } - - Ok(()) - })?; - }; - - diagnostics.sort_by(|a, b| { - a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal) - }); + }) + }) + }) + }, + ) + }) + { + if let (Ok(name), Ok(location)) = + (files.name(file_id), files.location(file_id, range.start)) + { + let diagnostic = ( + name.clone(), + level, + location.line_number, + location.column_number, + text.clone(), + ); + diagnostics.push(diagnostic); + } else { + // todo: no location found + } + } + }); + // todo: no location found + } - diagnostics.iter().for_each(|diagnostic| { - println!( - "{}:{}:{}: {}: {}", - diagnostic.0, diagnostic.2, diagnostic.3, diagnostic.1, diagnostic.4 - ) - }); Ok(()) - })?; + })?; + }; + diagnostics.sort_by(|a, b| { + a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal) + }); + + diagnostics.iter().for_each(|diagnostic| { + println!( + "{}:{}:{}: {}: {}", + diagnostic.0, diagnostic.2, diagnostic.3, diagnostic.1, diagnostic.4 + ) + }); Ok(()) + })?; + + Ok(()) } -fn to_writer_pretty(sarif: &sarif::Sarif, color: ColorOption) -> Result<()> { - let color_choice; - match color { - ColorOption::Always => color_choice = ColorChoice::Always, - ColorOption::AlwaysAnsi => color_choice = ColorChoice::AlwaysAnsi, - ColorOption::Auto => color_choice = ColorChoice::Auto, - ColorOption::Never => color_choice = ColorChoice::Never, - } +fn to_writer_pretty(sarif: &sarif::Sarif, always_ansi: bool) -> Result<()> { + let mut color_choice = ColorChoice::Auto; + if always_ansi == true { + color_choice = ColorChoice::AlwaysAnsi; + } + + let mut writer = StandardStream::stdout(color_choice); + let mut files = SimpleFiles::new(); + let config = codespan_reporting::term::Config::default(); + let mut message_counter = (0, 0, 0); + sarif.runs.iter().try_for_each(|run| -> Result<()> { + if let Some(results) = run.results.as_ref() { + results.iter().try_for_each(|result| -> Result<()> { + let rules = vec![]; + let rules = run.tool.driver.rules.as_ref().unwrap_or(&rules); + let level = resolve_level(rules, run, result); + let mut diagnostic: Diagnostic = Diagnostic::new(match level { + ResultLevel::Note => diagnostic::Severity::Note, + ResultLevel::Warning => diagnostic::Severity::Warning, + ResultLevel::Error => diagnostic::Severity::Error, + _ => diagnostic::Severity::Warning, + }); + if let Some(message) = resolve_message_text_from_result(result, run) { + diagnostic.message = message; + } + if let Some(text) = resolve_short_description_from_result(rules, result) + { + diagnostic.notes.push(text); + } + if let Some(text) = resolve_full_description_from_result(rules, result) + { + diagnostic.notes.push(text); + } - let mut writer = StandardStream::stdout(color_choice); - let mut files = SimpleFiles::new(); - let config = codespan_reporting::term::Config::default(); - let mut message_counter = (0, 0, 0); - sarif.runs.iter().try_for_each(|run| -> Result<()> { - if let Some(results) = run.results.as_ref() { - results.iter().try_for_each(|result| -> Result<()> { - let rules = vec![]; - let rules = run.tool.driver.rules.as_ref().unwrap_or(&rules); - let level = resolve_level(rules, run, result); - let mut diagnostic: Diagnostic = Diagnostic::new(match level { - ResultLevel::Note => diagnostic::Severity::Note, - ResultLevel::Warning => diagnostic::Severity::Warning, - ResultLevel::Error => diagnostic::Severity::Error, - _ => diagnostic::Severity::Warning, - }); - if let Some(message) = resolve_message_text_from_result(result, run) { - diagnostic.message = message; - } - if let Some(text) = resolve_short_description_from_result(rules, result) - { - diagnostic.notes.push(text); - } - if let Some(text) = resolve_full_description_from_result(rules, result) - { - diagnostic.notes.push(text); - } - - if let Some(locations) = result.locations.as_ref() { - locations.iter().for_each(|location| { - if let Some((file_id, range)) = location - .physical_location - .as_ref() - .and_then(|physical_location| { - physical_location.artifact_location.as_ref().and_then( - |artifact_location| { - artifact_location.uri.as_ref().and_then(|uri| { - physical_location.region.as_ref().and_then(|region| { - get_physical_location_contents(physical_location, run) - .ok() - .and_then(|contents| { - let file_id = files.add(uri, contents); - if let (Some(range_start), Some(range_end)) = - get_byte_range(file_id, &files, region) - { - Some((file_id, range_start..range_end)) - } else { - None - } - }) - }) - }) - }, - ) - }) - { - diagnostic.labels.push(Label::primary(file_id, range)); - } - }); - } - - if let Some(locations) = result.related_locations.as_ref() { - locations.iter().for_each(|location| { - if let Some((file_id, range, message)) = location - .physical_location - .as_ref() - .and_then(|physical_location| { - physical_location.artifact_location.as_ref().and_then( - |artifact_location| { - artifact_location.uri.as_ref().and_then(|uri| { - physical_location.region.as_ref().and_then(|region| { - get_physical_location_contents(physical_location, run) - .ok() - .and_then(|contents| { - let file_id = files.add(uri, contents); - if let (Some(range_start), Some(range_end)) = - get_byte_range(file_id, &files, region) - { - Some(( - file_id, - range_start..range_end, - location - .message - .as_ref() - .and_then(|x| x.text.clone()), - )) - } else { - None - } - }) - }) - }) - }, - ) - }) - { - diagnostic.labels.push( - Label::secondary(file_id, range) - .with_message(message.unwrap_or("".to_string())), - ); - } - }); - } - - term::emit(&mut writer.lock(), &config, &files, &diagnostic)?; - match diagnostic.severity { - codespan_reporting::diagnostic::Severity::Note => { - message_counter.0 += 1 - } - codespan_reporting::diagnostic::Severity::Warning => { - message_counter.1 += 1 - } - codespan_reporting::diagnostic::Severity::Error => { - message_counter.2 += 1 - } - _ => {} - } - Ok(()) - })?; - }; - Ok(()) - })?; - - if message_counter.1 > 0 { - writer - .set_color(ColorSpec::new().set_fg(Some(Color::Yellow)).set_bold(true))?; - writer.write_all("warning".as_bytes())?; - writer.reset()?; - writer.set_color(ColorSpec::new().set_bold(true))?; - writer.write_all( - format!(": {} warnings emitted\n", message_counter.1).as_bytes(), - )?; - writer.reset()?; - } + if let Some(locations) = result.locations.as_ref() { + locations.iter().for_each(|location| { + if let Some((file_id, range)) = location + .physical_location + .as_ref() + .and_then(|physical_location| { + physical_location.artifact_location.as_ref().and_then( + |artifact_location| { + artifact_location.uri.as_ref().and_then(|uri| { + physical_location.region.as_ref().and_then(|region| { + get_physical_location_contents(physical_location, run) + .ok() + .and_then(|contents| { + let file_id = files.add(uri, contents); + if let (Some(range_start), Some(range_end)) = + get_byte_range(file_id, &files, region) + { + Some((file_id, range_start..range_end)) + } else { + None + } + }) + }) + }) + }, + ) + }) + { + diagnostic.labels.push(Label::primary(file_id, range)); + } + }); + } - if message_counter.2 > 0 { - writer - .set_color(ColorSpec::new().set_fg(Some(Color::Red)).set_bold(true))?; - writer.write_all("error".as_bytes())?; - writer.reset()?; - writer.set_color(ColorSpec::new().set_bold(true))?; - writer.write_all( - format!(": {} errors emitted\n", message_counter.1).as_bytes(), - )?; - writer.reset()?; - } + if let Some(locations) = result.related_locations.as_ref() { + locations.iter().for_each(|location| { + if let Some((file_id, range, message)) = location + .physical_location + .as_ref() + .and_then(|physical_location| { + physical_location.artifact_location.as_ref().and_then( + |artifact_location| { + artifact_location.uri.as_ref().and_then(|uri| { + physical_location.region.as_ref().and_then(|region| { + get_physical_location_contents(physical_location, run) + .ok() + .and_then(|contents| { + let file_id = files.add(uri, contents); + if let (Some(range_start), Some(range_end)) = + get_byte_range(file_id, &files, region) + { + Some(( + file_id, + range_start..range_end, + location + .message + .as_ref() + .and_then(|x| x.text.clone()), + )) + } else { + None + } + }) + }) + }) + }, + ) + }) + { + diagnostic.labels.push( + Label::secondary(file_id, range) + .with_message(message.unwrap_or("".to_string())), + ); + } + }); + } + term::emit(&mut writer.lock(), &config, &files, &diagnostic)?; + match diagnostic.severity { + codespan_reporting::diagnostic::Severity::Note => { + message_counter.0 += 1 + } + codespan_reporting::diagnostic::Severity::Warning => { + message_counter.1 += 1 + } + codespan_reporting::diagnostic::Severity::Error => { + message_counter.2 += 1 + } + _ => {} + } + Ok(()) + })?; + }; Ok(()) + })?; + + if message_counter.1 > 0 { + writer + .set_color(ColorSpec::new().set_fg(Some(Color::Yellow)).set_bold(true))?; + writer.write_all("warning".as_bytes())?; + writer.reset()?; + writer.set_color(ColorSpec::new().set_bold(true))?; + writer.write_all( + format!(": {} warnings emitted\n", message_counter.1).as_bytes(), + )?; + writer.reset()?; + } + + if message_counter.2 > 0 { + writer + .set_color(ColorSpec::new().set_fg(Some(Color::Red)).set_bold(true))?; + writer.write_all("error".as_bytes())?; + writer.reset()?; + writer.set_color(ColorSpec::new().set_bold(true))?; + writer.write_all( + format!(": {} errors emitted\n", message_counter.1).as_bytes(), + )?; + writer.reset()?; + } + + Ok(()) } #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] enum MessageFormat { - Plain, - Pretty, -} - -/// Read docs of termcolor's ColorChoice -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] -enum ColorOption { - Always, - AlwaysAnsi, - Auto, - Never, + Plain, + Pretty, } #[derive(Parser, Debug)] #[command( -version, -about = "Pretty print SARIF results", -after_help = "The expected input is a SARIF file (ex. cat foo.sarif | sarif-fmt).", -long_about = None + version, + about = "Pretty print SARIF results", + after_help = "The expected input is a SARIF file (ex. cat foo.sarif | sarif-fmt).", + long_about = None )] struct Args { - /// One of plain or pretty - #[arg(short, long, value_enum, default_value = "pretty")] - message_format: MessageFormat, - /// input file; reads from stdin if none is given - #[arg(short, long)] - input: Option, - /// Allows to override coloring engine, e.g. to force color in CI/CD environments - #[arg(short, long, value_enum, default_value = "auto")] - color: ColorOption, + /// One of plain or pretty + #[arg(short, long, value_enum, default_value = "pretty")] + message_format: MessageFormat, + /// input file; reads from stdin if none is given + #[arg(short, long)] + input: Option, + /// Always ANSI coloring (useful for CI) + #[arg(short, long)] + always_ansi: bool, } fn main() -> Result<()> { - let args = Args::parse(); - - let read = match args.input { - Some(path) => Box::new(File::open(path)?) as Box, - None => Box::new(std::io::stdin()) as Box, - }; - let reader = BufReader::new(read); - let sarif = process(reader)?; - match args.message_format { - MessageFormat::Plain => to_writer_plain(&sarif), - MessageFormat::Pretty => to_writer_pretty(&sarif, args.color), - } + let args = Args::parse(); + + let read = match args.input { + Some(path) => Box::new(File::open(path)?) as Box, + None => Box::new(std::io::stdin()) as Box, + }; + let reader = BufReader::new(read); + let sarif = process(reader)?; + match args.message_format { + MessageFormat::Plain => to_writer_plain(&sarif), + MessageFormat::Pretty => to_writer_pretty(&sarif, args.always_ansi), + } }