mod handle_text_tree_line;
mod total_package_counts;
use crate::format::emoji_symbols::EmojiSymbols;
use crate::format::print_config::{colorize, OutputFormat, PrintConfig};
use crate::format::CrateDetectionStatus;
use crate::mapping::CargoMetadataParameters;
use crate::scan::{GeigerContext, ScanResult};
use crate::tree::TextTreeLine;
use handle_text_tree_line::{
text_tree_line_extra_deps_group_to_table_line_string,
text_tree_line_package_to_table_line_string, HandlePackageParameters,
};
use total_package_counts::TotalPackageCounts;
use cargo_geiger_serde::{Count, CounterBlock};
use colored::ColoredString;
use std::collections::HashSet;
use std::path::PathBuf;
pub const UNSAFE_COUNTERS_HEADER: [&str; 6] = [
"Functions ",
"Expressions ",
"Impls ",
"Traits ",
"Methods ",
"Dependency",
];
pub fn create_table_from_text_tree_lines(
cargo_metadata_parameters: &CargoMetadataParameters,
table_parameters: &TableParameters,
text_tree_lines: Vec<TextTreeLine>,
) -> ScanResult {
let mut table_lines = Vec::<String>::new();
let mut total_package_counts = TotalPackageCounts::new();
let mut warning_count = 0;
let mut visited_package_ids = HashSet::new();
let emoji_symbols =
EmojiSymbols::new(table_parameters.print_config.output_format);
let mut handle_package_parameters = HandlePackageParameters {
total_package_counts: &mut total_package_counts,
visited_package_ids: &mut visited_package_ids,
warning_count: &mut warning_count,
};
for table_line in text_tree_lines.into_iter().filter_map(|text_tree_line| {
match text_tree_line {
TextTreeLine::ExtraDepsGroup {
kind: dep_kind,
tree_vines,
} => text_tree_line_extra_deps_group_to_table_line_string(
dep_kind, tree_vines,
),
TextTreeLine::Package {
id: package_id,
tree_vines,
} => text_tree_line_package_to_table_line_string(
cargo_metadata_parameters,
&emoji_symbols,
&mut handle_package_parameters,
package_id,
table_parameters,
tree_vines,
),
}
}) {
table_lines.push(table_line);
}
table_lines.push(String::new());
let total_detection_status =
total_package_counts.get_total_detection_status();
table_lines.push(format!(
"{}",
table_footer(
total_package_counts.total_counter_block,
total_package_counts.total_unused_counter_block,
table_parameters.print_config.output_format,
total_detection_status
)
));
table_lines.push(String::new());
ScanResult {
scan_output_lines: table_lines,
warning_count,
}
}
pub struct TableParameters<'a> {
pub geiger_context: &'a GeigerContext,
pub print_config: &'a PrintConfig,
pub rs_files_used: &'a HashSet<PathBuf>,
}
fn table_footer_unsafe_counts(
used: CounterBlock,
not_used: CounterBlock,
output_format: OutputFormat,
status: CrateDetectionStatus,
) -> ColoredString {
let fmt = |used: &Count, not_used: &Count| {
format!("{}/{}", used.unsafe_, used.unsafe_ + not_used.unsafe_)
};
let output = format!(
"{: <10} {: <12} {: <6} {: <7} {: <7}",
fmt(&used.functions, ¬_used.functions),
fmt(&used.exprs, ¬_used.exprs),
fmt(&used.item_impls, ¬_used.item_impls),
fmt(&used.item_traits, ¬_used.item_traits),
fmt(&used.methods, ¬_used.methods),
);
colorize(&status, output_format, output)
}
fn table_footer_safe_ratio(
used: CounterBlock,
not_used: CounterBlock,
output_format: OutputFormat,
status: CrateDetectionStatus,
) -> ColoredString {
let fmt = |used: &Count, not_used: &Count| {
format!(
"{:>5}/{:<}={:.2}%",
(used.safe + not_used.safe),
(used.safe + used.unsafe_ + not_used.unsafe_ + not_used.safe),
if used.safe + used.unsafe_ + not_used.unsafe_ + not_used.safe == 0
{
100.0
} else {
(100.00 * (used.safe + not_used.safe) as f32)
/ ((used.safe
+ used.unsafe_
+ not_used.unsafe_
+ not_used.safe) as f32)
}
)
};
let output = format!(
"{: <12} {: <18} {: <18} {: <12} {: <12}",
fmt(&used.functions, ¬_used.functions),
fmt(&used.exprs, ¬_used.exprs),
fmt(&used.item_impls, ¬_used.item_impls),
fmt(&used.item_traits, ¬_used.item_traits),
fmt(&used.methods, ¬_used.methods),
);
colorize(&status, output_format, output)
}
fn table_footer(
used: CounterBlock,
not_used: CounterBlock,
output_format: OutputFormat,
status: CrateDetectionStatus,
) -> ColoredString {
match output_format {
OutputFormat::Ratio => {
table_footer_safe_ratio(used, not_used, output_format, status)
}
_ => table_footer_unsafe_counts(used, not_used, output_format, status),
}
}
fn table_row(
used: &CounterBlock,
not_used: &CounterBlock,
output_format: OutputFormat,
) -> String {
match output_format {
OutputFormat::Ratio => {
let fmt = |used: &Count, not_used: &Count| {
format!(
"{:>5}/{:<}={:.2}%",
(used.safe + not_used.safe),
(used.safe
+ used.unsafe_
+ not_used.unsafe_
+ not_used.safe),
if used.safe
+ used.unsafe_
+ not_used.unsafe_
+ not_used.safe
== 0
{
100.0
} else {
(100.00 * (used.safe + not_used.safe) as f32)
/ ((used.safe
+ used.unsafe_
+ not_used.unsafe_
+ not_used.safe)
as f32)
}
)
};
format!(
"{: <12} {: <18} {: <18} {: <12} {: <12}",
fmt(&used.functions, ¬_used.functions),
fmt(&used.exprs, ¬_used.exprs),
fmt(&used.item_impls, ¬_used.item_impls),
fmt(&used.item_traits, ¬_used.item_traits),
fmt(&used.methods, ¬_used.methods)
)
}
_ => {
let fmt = |used: &Count, not_used: &Count| {
format!("{}/{}", used.unsafe_, used.unsafe_ + not_used.unsafe_)
};
format!(
"{: <10} {: <12} {: <6} {: <7} {: <7}",
fmt(&used.functions, ¬_used.functions),
fmt(&used.exprs, ¬_used.exprs),
fmt(&used.item_impls, ¬_used.item_impls),
fmt(&used.item_traits, ¬_used.item_traits),
fmt(&used.methods, ¬_used.methods)
)
}
}
}
fn table_row_empty() -> String {
let headers_but_last =
&UNSAFE_COUNTERS_HEADER[..UNSAFE_COUNTERS_HEADER.len() - 1];
let n = headers_but_last
.iter()
.map(|s| s.len())
.sum::<usize>()
+ headers_but_last.len() + 4 + 2 + 1; " ".repeat(n)
}
#[cfg(test)]
mod table_tests {
use super::*;
use crate::scan::{unsafe_stats, PackageMetrics, RsFileMetricsWrapper};
use geiger::RsFileMetrics;
use rstest::*;
use std::collections::HashMap;
use std::path::Path;
use strum::IntoEnumIterator;
#[rstest(
input_output_format,
expected_line,
case(
OutputFormat::Ascii,
String::from("2/4 4/8 6/12 8/16 10/20 ")
),
case(
OutputFormat::GitHubMarkdown,
String::from("2/4 4/8 6/12 8/16 10/20 ")
),
case(
OutputFormat::Ratio,
String::from(" 2/6=33.33% 6/14=42.86% 10/22=45.45% 14/30=46.67% 18/38=47.37%")
),
case(
OutputFormat::Utf8,
String::from("2/4 4/8 6/12 8/16 10/20 ")
)
)]
fn table_footer_test(
input_output_format: OutputFormat,
expected_line: String,
) {
let used_counter_block = create_counter_block();
let not_used_counter_block = create_counter_block();
for crate_detection_status in CrateDetectionStatus::iter() {
let table_footer = table_footer(
used_counter_block.clone(),
not_used_counter_block.clone(),
input_output_format,
crate_detection_status.clone(),
);
assert_eq!(
table_footer,
colorize(
&crate_detection_status,
input_output_format,
expected_line.clone()
)
);
}
}
#[rstest]
fn table_row_test() {
let mut rs_path_to_metrics =
HashMap::<PathBuf, RsFileMetricsWrapper>::new();
rs_path_to_metrics.insert(
Path::new("package_1_path").to_path_buf(),
create_rs_file_metrics_wrapper(true, true),
);
rs_path_to_metrics.insert(
Path::new("package_2_path").to_path_buf(),
create_rs_file_metrics_wrapper(true, false),
);
rs_path_to_metrics.insert(
Path::new("package_3_path").to_path_buf(),
create_rs_file_metrics_wrapper(false, false),
);
let package_metrics = PackageMetrics { rs_path_to_metrics };
let rs_files_used: HashSet<PathBuf> = [
Path::new("package_1_path").to_path_buf(),
Path::new("package_3_path").to_path_buf(),
]
.iter()
.cloned()
.collect();
let unsafety = unsafe_stats(&package_metrics, &rs_files_used);
let table_row =
table_row(&unsafety.used, &unsafety.unused, OutputFormat::Ascii);
assert_eq!(table_row, "4/6 8/12 12/18 16/24 20/30 ");
}
#[rstest]
fn table_row_empty_test() {
let empty_table_row = table_row_empty();
assert_eq!(empty_table_row.len(), 55);
}
#[rstest(
input_none_detected_forbids_unsafe,
input_none_detected_allows_unsafe,
input_unsafe_detected,
expected_crate_detection_status,
case(0, 0, 1, CrateDetectionStatus::UnsafeDetected),
case(1, 0, 0, CrateDetectionStatus::NoneDetectedForbidsUnsafe),
case(4, 1, 0, CrateDetectionStatus::NoneDetectedAllowsUnsafe)
)]
fn total_package_counts_get_total_detection_status_tests(
input_none_detected_forbids_unsafe: i32,
input_none_detected_allows_unsafe: i32,
input_unsafe_detected: i32,
expected_crate_detection_status: CrateDetectionStatus,
) {
let total_detection_status = TotalPackageCounts {
none_detected_forbids_unsafe: input_none_detected_forbids_unsafe,
none_detected_allows_unsafe: input_none_detected_allows_unsafe,
unsafe_detected: input_unsafe_detected,
total_counter_block: CounterBlock::default(),
total_unused_counter_block: CounterBlock::default(),
};
assert_eq!(
total_detection_status.get_total_detection_status(),
expected_crate_detection_status
);
}
fn create_rs_file_metrics_wrapper(
forbids_unsafe: bool,
is_crate_entry_point: bool,
) -> RsFileMetricsWrapper {
RsFileMetricsWrapper {
metrics: RsFileMetrics {
counters: create_counter_block(),
forbids_unsafe,
},
is_crate_entry_point,
}
}
fn create_counter_block() -> CounterBlock {
CounterBlock {
functions: Count {
safe: 1,
unsafe_: 2,
},
exprs: Count {
safe: 3,
unsafe_: 4,
},
item_impls: Count {
safe: 5,
unsafe_: 6,
},
item_traits: Count {
safe: 7,
unsafe_: 8,
},
methods: Count {
safe: 9,
unsafe_: 10,
},
}
}
}