use crate::parser::{SarifError, SarifResult as ParseResult};
use crate::types::{Level, ReportingDescriptor, Result as SarifResult, Run, SarifLog};
use crate::utils::indexing::{ResultLocation, SarifIndex};
use regex::Regex;
use std::collections::{HashMap, HashSet};
use std::time::Instant;
#[derive(Debug, Clone)]
pub struct SarifQuery {
pub tool_filters: ToolFilters,
pub rule_filters: RuleFilters,
pub result_filters: ResultFilters,
pub location_filters: LocationFilters,
pub text_filters: TextFilters,
pub aggregation: AggregationSettings,
pub ordering: ResultOrdering,
pub pagination: PaginationSettings,
}
#[derive(Debug, Clone)]
pub struct ToolFilters {
pub include_tools: Option<HashSet<String>>,
pub exclude_tools: HashSet<String>,
pub version_patterns: Vec<String>,
pub organizations: Option<HashSet<String>>,
}
#[derive(Debug, Clone)]
pub struct RuleFilters {
pub include_rules: Option<HashSet<String>>,
pub exclude_rules: HashSet<String>,
pub categories: Option<HashSet<String>>,
pub tags: Option<HashSet<String>>,
pub config_levels: Option<HashSet<String>>,
}
#[derive(Debug, Clone)]
pub struct ResultFilters {
pub levels: Option<HashSet<Level>>,
pub min_level: Option<Level>,
pub kinds: Option<HashSet<String>>,
pub has_fixes: Option<bool>,
pub guids: Option<HashSet<String>>,
pub correlation_guids: Option<HashSet<String>>,
}
#[derive(Debug, Clone)]
pub struct LocationFilters {
pub file_patterns: Vec<String>,
pub exclude_file_patterns: Vec<String>,
pub line_ranges: Vec<LineRange>,
pub logical_locations: Option<HashSet<String>>,
pub uri_schemes: Option<HashSet<String>>,
}
#[derive(Debug, Clone)]
pub struct LineRange {
pub start: u32,
pub end: Option<u32>,
pub file_pattern: Option<String>,
}
#[derive(Debug, Clone)]
pub struct TextFilters {
pub message_text: Option<TextSearch>,
pub rule_description: Option<TextSearch>,
pub file_content: Option<TextSearch>,
pub snippet_content: Option<TextSearch>,
}
#[derive(Debug, Clone)]
pub struct TextSearch {
pub pattern: String,
pub case_sensitive: bool,
pub use_regex: bool,
pub whole_words: bool,
}
#[derive(Debug, Clone)]
pub struct AggregationSettings {
pub group_by: Vec<GroupByField>,
pub counts: CountSettings,
pub include_stats: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub enum GroupByField {
Tool,
Rule,
Level,
File,
Category,
Tag,
}
#[derive(Debug, Clone)]
pub struct CountSettings {
pub results: bool,
pub files: bool,
pub tools: bool,
pub rules: bool,
}
#[derive(Debug, Clone)]
pub struct ResultOrdering {
pub primary: OrderField,
pub secondary: Vec<OrderField>,
}
#[derive(Debug, Clone)]
pub struct OrderField {
pub field: SortField,
pub direction: SortDirection,
}
#[derive(Debug, Clone, PartialEq)]
pub enum SortField {
Tool,
Rule,
Level,
File,
Line,
Column,
Message,
Timestamp,
}
#[derive(Debug, Clone, PartialEq)]
pub enum SortDirection {
Ascending,
Descending,
}
#[derive(Debug, Clone)]
pub struct PaginationSettings {
pub page: usize,
pub page_size: usize,
pub max_results: Option<usize>,
}
#[derive(Debug, Clone)]
pub struct QueryResults {
pub results: Vec<(SarifResult, ResultLocation)>,
pub aggregations: QueryAggregations,
pub stats: QueryStats,
pub total_count: usize,
pub has_more: bool,
}
#[derive(Debug, Clone)]
pub struct QueryAggregations {
pub groups: HashMap<String, GroupedResults>,
pub counts: CountSummary,
pub level_distribution: HashMap<Level, usize>,
pub tool_distribution: HashMap<String, usize>,
pub file_distribution: HashMap<String, usize>,
}
#[derive(Debug, Clone)]
pub struct GroupedResults {
pub key: String,
pub results: Vec<(SarifResult, ResultLocation)>,
pub count: usize,
}
#[derive(Debug, Clone)]
pub struct CountSummary {
pub total_results: usize,
pub unique_files: usize,
pub unique_tools: usize,
pub unique_rules: usize,
}
#[derive(Debug, Clone)]
pub struct QueryStats {
pub execution_time: std::time::Duration,
pub results_evaluated: usize,
pub results_matched: usize,
pub index_lookup_time: std::time::Duration,
pub filtering_time: std::time::Duration,
pub aggregation_time: std::time::Duration,
}
pub struct SarifQueryExecutor {
index: SarifIndex,
log: SarifLog,
}
impl SarifQueryExecutor {
pub fn new(log: SarifLog) -> ParseResult<Self> {
let index = SarifIndex::from_sarif_log(&log);
Ok(Self { index, log })
}
pub fn from_index(index: SarifIndex, log: SarifLog) -> Self {
Self { index, log }
}
pub fn execute(&self, query: &SarifQuery) -> ParseResult<QueryResults> {
let start_time = Instant::now();
let mut stats = QueryStats {
execution_time: std::time::Duration::ZERO,
results_evaluated: 0,
results_matched: 0,
index_lookup_time: std::time::Duration::ZERO,
filtering_time: std::time::Duration::ZERO,
aggregation_time: std::time::Duration::ZERO,
};
let index_start = Instant::now();
let candidates = self.get_candidates(query)?;
stats.index_lookup_time = index_start.elapsed();
stats.results_evaluated = candidates.len();
let filter_start = Instant::now();
let filtered_results = self.apply_filters(&candidates, query)?;
stats.filtering_time = filter_start.elapsed();
stats.results_matched = filtered_results.len();
let ordered_results = self.apply_ordering(filtered_results, query);
let total_count = ordered_results.len();
let (paginated_results, has_more) = self.apply_pagination(ordered_results, query);
let agg_start = Instant::now();
let aggregations = if query.aggregation.include_stats {
self.generate_aggregations(&paginated_results, query)?
} else {
QueryAggregations::empty()
};
stats.aggregation_time = agg_start.elapsed();
stats.execution_time = start_time.elapsed();
Ok(QueryResults {
results: paginated_results,
aggregations,
stats,
total_count,
has_more,
})
}
fn get_candidates(
&self,
query: &SarifQuery,
) -> ParseResult<Vec<(SarifResult, ResultLocation)>> {
let mut candidates = Vec::new();
if query.tool_filters.include_tools.is_none()
&& query.rule_filters.include_rules.is_none()
&& query.result_filters.guids.is_none()
{
for (result, location) in self.index.results.values() {
candidates.push((result.clone(), location.clone()));
}
} else {
if let Some(ref tool_names) = query.tool_filters.include_tools {
for tool_name in tool_names {
if let Some(run_indices) = self.index.tool_to_runs.get(tool_name) {
for &run_index in run_indices {
if let Some(run) = self.log.runs.get(run_index)
&& let Some(ref results) = run.results
{
for (result_index, result) in results.iter().enumerate() {
let location = ResultLocation {
run_index,
result_index,
guid: result.guid.clone(),
rule_id: result.rule_id.clone(),
primary_artifact_uri: None,
};
candidates.push((result.clone(), location));
}
}
}
}
}
}
if let Some(ref rule_ids) = query.rule_filters.include_rules {
for rule_id in rule_ids {
if let Some(result_guids) = self.index.rule_to_results.get(rule_id) {
for result_guid in result_guids {
if let Some((result, location)) = self.index.results.get(result_guid) {
candidates.push((result.clone(), location.clone()));
}
}
}
}
}
if let Some(ref guids) = query.result_filters.guids {
for guid in guids {
if let Some((result, location)) = self.index.results.get(guid) {
candidates.push((result.clone(), location.clone()));
}
}
}
}
candidates.sort_by(|a, b| {
a.1.run_index
.cmp(&b.1.run_index)
.then_with(|| a.1.result_index.cmp(&b.1.result_index))
});
candidates.dedup_by(|a, b| {
a.1.run_index == b.1.run_index && a.1.result_index == b.1.result_index
});
Ok(candidates)
}
fn apply_filters(
&self,
candidates: &[(SarifResult, ResultLocation)],
query: &SarifQuery,
) -> ParseResult<Vec<(SarifResult, ResultLocation)>> {
let mut filtered = Vec::new();
for (result, location) in candidates {
if self.matches_filters(result, location, query)? {
filtered.push((result.clone(), location.clone()));
}
}
Ok(filtered)
}
fn matches_filters(
&self,
result: &SarifResult,
location: &ResultLocation,
query: &SarifQuery,
) -> ParseResult<bool> {
let run = &self.log.runs[location.run_index];
if !self.matches_tool_filters(&run, &query.tool_filters) {
return Ok(false);
}
if !self.matches_rule_filters(result, &run, &query.rule_filters)? {
return Ok(false);
}
if !self.matches_result_filters(result, &query.result_filters) {
return Ok(false);
}
if !self.matches_location_filters(result, &query.location_filters)? {
return Ok(false);
}
if !self.matches_text_filters(result, &run, &query.text_filters)? {
return Ok(false);
}
Ok(true)
}
fn matches_tool_filters(&self, run: &Run, filters: &ToolFilters) -> bool {
let tool_name = &run.tool.driver.name;
if let Some(ref include_tools) = filters.include_tools
&& !include_tools.contains(tool_name)
{
return false;
}
if filters.exclude_tools.contains(tool_name) {
return false;
}
if !filters.version_patterns.is_empty() {
if let Some(ref version) = run.tool.driver.version {
let matches_version = filters.version_patterns.iter().any(|pattern| {
if let Ok(regex) = Regex::new(pattern) {
regex.is_match(version)
} else {
version.contains(pattern)
}
});
if !matches_version {
return false;
}
} else {
return false;
}
}
if let Some(ref organizations) = filters.organizations {
if let Some(ref organization) = run.tool.driver.organization {
if !organizations.contains(organization) {
return false;
}
} else {
return false;
}
}
true
}
fn matches_rule_filters(
&self,
result: &SarifResult,
run: &Run,
filters: &RuleFilters,
) -> ParseResult<bool> {
if let Some(ref rule_id) = result.rule_id {
if let Some(ref include_rules) = filters.include_rules
&& !include_rules.contains(rule_id)
{
return Ok(false);
}
if filters.exclude_rules.contains(rule_id) {
return Ok(false);
}
let rule_metadata = self.get_rule_metadata(rule_id, run);
if let Some(ref categories) = filters.categories {
if let Some(ref rule) = rule_metadata
&& let Some(ref props) = rule.properties
&& let Some(category) = props.get("category")
&& let Some(category_str) = category.as_str()
&& !categories.contains(category_str)
{
return Ok(false);
} else if rule_metadata.is_none() {
return Ok(false);
}
}
if let Some(ref tags) = filters.tags {
if let Some(ref rule) = rule_metadata
&& let Some(ref rule_tags) = rule.properties
&& let Some(tag_array) = rule_tags.get("tags")
&& let Some(tag_vec) = tag_array.as_array()
{
let rule_tag_strings: HashSet<String> = tag_vec
.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect();
if !tags.intersection(&rule_tag_strings).any(|_| true) {
return Ok(false);
}
} else if rule_metadata.is_none() {
return Ok(false);
}
}
}
Ok(true)
}
fn matches_result_filters(&self, result: &SarifResult, filters: &ResultFilters) -> bool {
if let Some(ref levels) = filters.levels {
if let Some(ref level) = result.level {
if !levels.contains(level) {
return false;
}
} else {
if !levels.contains(&Level::Warning) {
return false;
}
}
}
if let Some(ref min_level) = filters.min_level {
let result_level = result.level.as_ref().unwrap_or(&Level::Warning);
if !self.is_level_at_least(result_level, min_level) {
return false;
}
}
if let Some(ref kinds) = filters.kinds
&& let Some(ref kind) = result.kind
&& !kinds.contains(&kind.to_string())
{
return false;
}
if let Some(has_fixes) = filters.has_fixes {
let result_has_fixes =
result.fixes.is_some() && !result.fixes.as_ref().unwrap().is_empty();
if has_fixes != result_has_fixes {
return false;
}
}
if let Some(ref guids) = filters.guids {
if let Some(ref guid) = result.guid {
if !guids.contains(guid) {
return false;
}
} else {
return false;
}
}
if let Some(ref correlation_guids) = filters.correlation_guids {
if let Some(ref correlation_guid) = result.correlation_guid {
if !correlation_guids.contains(correlation_guid) {
return false;
}
} else {
return false;
}
}
true
}
fn matches_location_filters(
&self,
result: &SarifResult,
filters: &LocationFilters,
) -> ParseResult<bool> {
if let Some(ref locations) = result.locations {
for location in locations {
if let Some(ref physical_location) = location.physical_location {
if let Some(ref artifact_location) = physical_location.artifact_location
&& let Some(ref uri) = artifact_location.uri
{
if !filters.file_patterns.is_empty() {
let matches_include = filters
.file_patterns
.iter()
.any(|pattern| self.matches_glob_pattern(uri, pattern));
if !matches_include {
continue;
}
}
if filters
.exclude_file_patterns
.iter()
.any(|pattern| self.matches_glob_pattern(uri, pattern))
{
continue;
}
if !filters.line_ranges.is_empty()
&& let Some(ref region) = physical_location.region
&& let Some(start_line) = region.start_line
{
let matches_line_range = filters.line_ranges.iter().any(|range| {
if let Some(ref file_pattern) = range.file_pattern
&& !self.matches_glob_pattern(uri, file_pattern)
{
return false;
}
let line = start_line as u32;
line >= range.start && range.end.map_or(true, |end| line <= end)
});
if !matches_line_range {
continue;
}
}
return Ok(true);
}
}
if let Some(ref logical_locations) = location.logical_locations {
for logical_location in logical_locations {
if let Some(ref filter_logical_locations) = filters.logical_locations {
if let Some(ref name) = logical_location.name
&& filter_logical_locations.contains(name)
{
return Ok(true);
}
if let Some(ref fully_qualified_name) =
logical_location.fully_qualified_name
&& filter_logical_locations.contains(fully_qualified_name)
{
return Ok(true);
}
}
}
}
}
if !filters.file_patterns.is_empty()
|| !filters.exclude_file_patterns.is_empty()
|| !filters.line_ranges.is_empty()
|| filters.logical_locations.is_some()
{
return Ok(false);
}
}
Ok(true)
}
fn matches_text_filters(
&self,
result: &SarifResult,
run: &Run,
filters: &TextFilters,
) -> ParseResult<bool> {
if let Some(ref message_search) = filters.message_text {
if let Some(ref text) = result.message.text {
if !self.matches_text_search(text, message_search)? {
return Ok(false);
}
} else {
return Ok(false);
}
}
if let Some(ref rule_description_search) = filters.rule_description {
if let Some(ref rule_id) = result.rule_id {
if let Some(rule) = self.get_rule_metadata(rule_id, run) {
if let Some(ref short_description) = rule.short_description {
if !self
.matches_text_search(&short_description.text, rule_description_search)?
{
return Ok(false);
}
} else {
return Ok(false);
}
} else {
return Ok(false);
}
} else {
return Ok(false);
}
}
Ok(true)
}
fn apply_ordering(
&self,
mut results: Vec<(SarifResult, ResultLocation)>,
query: &SarifQuery,
) -> Vec<(SarifResult, ResultLocation)> {
results.sort_by(|a, b| {
let primary_cmp =
self.compare_by_field(&a.0, &a.1, &b.0, &b.1, &query.ordering.primary);
if primary_cmp != std::cmp::Ordering::Equal {
return primary_cmp;
}
for secondary in &query.ordering.secondary {
let secondary_cmp = self.compare_by_field(&a.0, &a.1, &b.0, &b.1, secondary);
if secondary_cmp != std::cmp::Ordering::Equal {
return secondary_cmp;
}
}
std::cmp::Ordering::Equal
});
results
}
fn apply_pagination(
&self,
results: Vec<(SarifResult, ResultLocation)>,
query: &SarifQuery,
) -> (Vec<(SarifResult, ResultLocation)>, bool) {
let start_index = query.pagination.page * query.pagination.page_size;
let end_index = start_index + query.pagination.page_size;
let limited_results = if let Some(max) = query.pagination.max_results {
results.into_iter().take(max).collect()
} else {
results
};
let total_len = limited_results.len();
let has_more = end_index < total_len;
let paginated = limited_results
.into_iter()
.skip(start_index)
.take(query.pagination.page_size)
.collect();
(paginated, has_more)
}
fn generate_aggregations(
&self,
results: &[(SarifResult, ResultLocation)],
query: &SarifQuery,
) -> ParseResult<QueryAggregations> {
let mut groups = HashMap::new();
let mut level_dist = HashMap::new();
let mut tool_dist = HashMap::new();
let mut file_dist = HashMap::new();
let mut unique_files = HashSet::new();
let mut unique_tools = HashSet::new();
let mut unique_rules = HashSet::new();
for (result, location) in results {
let run = &self.log.runs[location.run_index];
for group_field in &query.aggregation.group_by {
let group_key = self.get_group_key(result, run, group_field);
let group_entry =
groups
.entry(group_key.clone())
.or_insert_with(|| GroupedResults {
key: group_key,
results: Vec::new(),
count: 0,
});
group_entry.results.push((result.clone(), location.clone()));
group_entry.count += 1;
}
let level = result.level.as_ref().unwrap_or(&Level::Warning);
*level_dist.entry(level.clone()).or_insert(0) += 1;
let tool_name = &run.tool.driver.name;
*tool_dist.entry(tool_name.clone()).or_insert(0) += 1;
unique_tools.insert(tool_name.clone());
if let Some(ref locations) = result.locations {
for loc in locations {
if let Some(ref phys_loc) = loc.physical_location
&& let Some(ref artifact_loc) = phys_loc.artifact_location
&& let Some(ref uri) = artifact_loc.uri
{
*file_dist.entry(uri.clone()).or_insert(0) += 1;
unique_files.insert(uri.clone());
}
}
}
if let Some(ref rule_id) = result.rule_id {
unique_rules.insert(rule_id.clone());
}
}
let counts = CountSummary {
total_results: results.len(),
unique_files: unique_files.len(),
unique_tools: unique_tools.len(),
unique_rules: unique_rules.len(),
};
Ok(QueryAggregations {
groups,
counts,
level_distribution: level_dist,
tool_distribution: tool_dist,
file_distribution: file_dist,
})
}
fn get_rule_metadata(&self, rule_id: &str, _run: &Run) -> Option<&ReportingDescriptor> {
self.index.rules.get(rule_id)
}
fn is_level_at_least(&self, level: &Level, min_level: &Level) -> bool {
let level_value = match level {
Level::None => 0,
Level::Note => 1,
Level::Warning => 2,
Level::Error => 3,
};
let min_value = match min_level {
Level::None => 0,
Level::Note => 1,
Level::Warning => 2,
Level::Error => 3,
};
level_value >= min_value
}
fn matches_glob_pattern(&self, text: &str, pattern: &str) -> bool {
if pattern == "*" {
return true;
}
if pattern.contains('*') {
let parts: Vec<&str> = pattern.split('*').collect();
if parts.len() == 2 {
let prefix = parts[0];
let suffix = parts[1];
return text.starts_with(prefix) && text.ends_with(suffix);
}
}
text == pattern
}
fn matches_text_search(&self, text: &str, search: &TextSearch) -> ParseResult<bool> {
let search_text = if search.case_sensitive {
text.to_string()
} else {
text.to_lowercase()
};
let pattern = if search.case_sensitive {
search.pattern.clone()
} else {
search.pattern.to_lowercase()
};
if search.use_regex {
let regex = Regex::new(&pattern)
.map_err(|e| SarifError::custom(format!("Invalid regex pattern: {}", e)))?;
Ok(regex.is_match(&search_text))
} else if search.whole_words {
let words: Vec<&str> = search_text.split_whitespace().collect();
Ok(words.contains(&pattern.as_str()))
} else {
Ok(search_text.contains(&pattern))
}
}
fn compare_by_field(
&self,
a_result: &SarifResult,
a_location: &ResultLocation,
b_result: &SarifResult,
b_location: &ResultLocation,
order: &OrderField,
) -> std::cmp::Ordering {
let comparison = match order.field {
SortField::Tool => {
let a_tool = &self.log.runs[a_location.run_index].tool.driver.name;
let b_tool = &self.log.runs[b_location.run_index].tool.driver.name;
a_tool.cmp(b_tool)
}
SortField::Rule => {
let a_rule = a_result.rule_id.as_deref().unwrap_or("");
let b_rule = b_result.rule_id.as_deref().unwrap_or("");
a_rule.cmp(b_rule)
}
SortField::Level => {
let a_level = a_result.level.as_ref().unwrap_or(&Level::Warning);
let b_level = b_result.level.as_ref().unwrap_or(&Level::Warning);
a_level.cmp(b_level)
}
SortField::File => {
let a_file = self.get_file_path(a_result);
let b_file = self.get_file_path(b_result);
a_file.cmp(&b_file)
}
SortField::Line => {
let a_line = self.get_line_number(a_result);
let b_line = self.get_line_number(b_result);
a_line.cmp(&b_line)
}
SortField::Column => {
let a_col = self.get_column_number(a_result);
let b_col = self.get_column_number(b_result);
a_col.cmp(&b_col)
}
SortField::Message => {
let a_msg = a_result.message.text.as_deref().unwrap_or("");
let b_msg = b_result.message.text.as_deref().unwrap_or("");
a_msg.cmp(b_msg)
}
SortField::Timestamp => {
a_location.result_index.cmp(&b_location.result_index)
}
};
match order.direction {
SortDirection::Ascending => comparison,
SortDirection::Descending => comparison.reverse(),
}
}
fn get_group_key(&self, result: &SarifResult, run: &Run, field: &GroupByField) -> String {
match field {
GroupByField::Tool => run.tool.driver.name.clone(),
GroupByField::Rule => result.rule_id.as_deref().unwrap_or("unknown").to_string(),
GroupByField::Level => {
format!("{:?}", result.level.as_ref().unwrap_or(&Level::Warning))
}
GroupByField::File => self.get_file_path(result),
GroupByField::Category => {
if let Some(ref rule_id) = result.rule_id
&& let Some(rule) = self.get_rule_metadata(rule_id, run)
&& let Some(ref props) = rule.properties
&& let Some(category) = props.get("category")
&& let Some(cat_str) = category.as_str()
{
return cat_str.to_string();
}
"unknown".to_string()
}
GroupByField::Tag => {
if let Some(ref rule_id) = result.rule_id
&& let Some(rule) = self.get_rule_metadata(rule_id, run)
&& let Some(ref props) = rule.properties
&& let Some(tags) = props.get("tags")
&& let Some(tag_array) = tags.as_array()
&& let Some(first_tag) = tag_array.first()
&& let Some(tag_str) = first_tag.as_str()
{
return tag_str.to_string();
}
"unknown".to_string()
}
}
}
fn get_file_path(&self, result: &SarifResult) -> String {
if let Some(ref locations) = result.locations {
for location in locations {
if let Some(ref physical_location) = location.physical_location
&& let Some(ref artifact_location) = physical_location.artifact_location
&& let Some(ref uri) = artifact_location.uri
{
return uri.clone();
}
}
}
"unknown".to_string()
}
fn get_line_number(&self, result: &SarifResult) -> i32 {
if let Some(ref locations) = result.locations {
for location in locations {
if let Some(ref physical_location) = location.physical_location
&& let Some(ref region) = physical_location.region
&& let Some(line) = region.start_line
{
return line;
}
}
}
0
}
fn get_column_number(&self, result: &SarifResult) -> i32 {
if let Some(ref locations) = result.locations {
for location in locations {
if let Some(ref physical_location) = location.physical_location
&& let Some(ref region) = physical_location.region
&& let Some(column) = region.start_column
{
return column;
}
}
}
0
}
}
impl QueryAggregations {
fn empty() -> Self {
Self {
groups: HashMap::new(),
counts: CountSummary {
total_results: 0,
unique_files: 0,
unique_tools: 0,
unique_rules: 0,
},
level_distribution: HashMap::new(),
tool_distribution: HashMap::new(),
file_distribution: HashMap::new(),
}
}
}
impl Default for SarifQuery {
fn default() -> Self {
Self {
tool_filters: ToolFilters::default(),
rule_filters: RuleFilters::default(),
result_filters: ResultFilters::default(),
location_filters: LocationFilters::default(),
text_filters: TextFilters::default(),
aggregation: AggregationSettings::default(),
ordering: ResultOrdering::default(),
pagination: PaginationSettings::default(),
}
}
}
impl Default for ToolFilters {
fn default() -> Self {
Self {
include_tools: None,
exclude_tools: HashSet::new(),
version_patterns: Vec::new(),
organizations: None,
}
}
}
impl Default for RuleFilters {
fn default() -> Self {
Self {
include_rules: None,
exclude_rules: HashSet::new(),
categories: None,
tags: None,
config_levels: None,
}
}
}
impl Default for ResultFilters {
fn default() -> Self {
Self {
levels: None,
min_level: None,
kinds: None,
has_fixes: None,
guids: None,
correlation_guids: None,
}
}
}
impl Default for LocationFilters {
fn default() -> Self {
Self {
file_patterns: Vec::new(),
exclude_file_patterns: Vec::new(),
line_ranges: Vec::new(),
logical_locations: None,
uri_schemes: None,
}
}
}
impl Default for TextFilters {
fn default() -> Self {
Self {
message_text: None,
rule_description: None,
file_content: None,
snippet_content: None,
}
}
}
impl Default for AggregationSettings {
fn default() -> Self {
Self {
group_by: Vec::new(),
counts: CountSettings {
results: true,
files: true,
tools: true,
rules: true,
},
include_stats: false,
}
}
}
impl Default for ResultOrdering {
fn default() -> Self {
Self {
primary: OrderField {
field: SortField::File,
direction: SortDirection::Ascending,
},
secondary: vec![
OrderField {
field: SortField::Line,
direction: SortDirection::Ascending,
},
OrderField {
field: SortField::Column,
direction: SortDirection::Ascending,
},
],
}
}
}
impl Default for PaginationSettings {
fn default() -> Self {
Self {
page: 0,
page_size: 100,
max_results: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::builder::SarifLogBuilder;
#[test]
fn test_basic_query() {
let log = SarifLogBuilder::single_error("test-tool", "Test error", "test.rs", 10)
.build_unchecked();
let executor = SarifQueryExecutor::new(log).unwrap();
let query = SarifQuery::default();
let results = executor.execute(&query).unwrap();
assert_eq!(results.total_count, 1);
assert_eq!(results.results.len(), 1);
assert!(!results.has_more);
}
#[test]
fn test_tool_filter() {
let log1 =
SarifLogBuilder::single_error("tool1", "Error 1", "file1.rs", 10).build_unchecked();
let log2 =
SarifLogBuilder::single_error("tool2", "Error 2", "file2.rs", 20).build_unchecked();
let merged_log = SarifLogBuilder::new()
.add_run(log1.runs.into_iter().next().unwrap())
.add_run(log2.runs.into_iter().next().unwrap())
.build_unchecked();
let executor = SarifQueryExecutor::new(merged_log).unwrap();
let mut include_tools = HashSet::new();
include_tools.insert("tool1".to_string());
let query = SarifQuery {
tool_filters: ToolFilters {
include_tools: Some(include_tools),
..Default::default()
},
..Default::default()
};
let results = executor.execute(&query).unwrap();
assert_eq!(results.total_count, 1);
assert_eq!(
results.results[0].0.message.text,
Some("Error 1".to_string())
);
}
#[test]
fn test_level_filter() {
let log = SarifLogBuilder::single_warning("test-tool", "Warning", "test.rs", 10)
.build_unchecked();
let executor = SarifQueryExecutor::new(log).unwrap();
let mut levels = HashSet::new();
levels.insert(Level::Error);
let query = SarifQuery {
result_filters: ResultFilters {
levels: Some(levels),
..Default::default()
},
..Default::default()
};
let results = executor.execute(&query).unwrap();
assert_eq!(results.total_count, 0);
}
#[test]
fn test_file_pattern_filter() {
let log = SarifLogBuilder::single_error("test-tool", "Error", "src/main.rs", 10)
.build_unchecked();
let executor = SarifQueryExecutor::new(log).unwrap();
let query = SarifQuery {
location_filters: LocationFilters {
file_patterns: vec!["src/*".to_string()],
..Default::default()
},
..Default::default()
};
let results = executor.execute(&query).unwrap();
assert_eq!(results.total_count, 1);
}
#[test]
fn test_pagination() {
let log = SarifLogBuilder::new()
.add_simple_run("tool", None::<String>)
.build_unchecked();
let executor = SarifQueryExecutor::new(log).unwrap();
let query = SarifQuery {
pagination: PaginationSettings {
page: 0,
page_size: 50,
max_results: Some(100),
},
..Default::default()
};
let results = executor.execute(&query).unwrap();
assert_eq!(results.results.len(), 0); assert!(!results.has_more);
}
#[test]
fn test_aggregation() {
let log =
SarifLogBuilder::single_error("test-tool", "Error", "test.rs", 10).build_unchecked();
let executor = SarifQueryExecutor::new(log).unwrap();
let query = SarifQuery {
aggregation: AggregationSettings {
group_by: vec![GroupByField::Tool, GroupByField::Level],
include_stats: true,
..Default::default()
},
..Default::default()
};
let results = executor.execute(&query).unwrap();
assert_eq!(results.aggregations.counts.total_results, 1);
assert_eq!(results.aggregations.counts.unique_tools, 1);
assert!(
results
.aggregations
.tool_distribution
.contains_key("test-tool")
);
}
#[test]
fn test_text_search() {
let log = SarifLogBuilder::single_error("test-tool", "Memory leak detected", "test.rs", 10)
.build_unchecked();
let executor = SarifQueryExecutor::new(log).unwrap();
let query = SarifQuery {
text_filters: TextFilters {
message_text: Some(TextSearch {
pattern: "memory".to_string(),
case_sensitive: false,
use_regex: false,
whole_words: false,
}),
..Default::default()
},
..Default::default()
};
let results = executor.execute(&query).unwrap();
assert_eq!(results.total_count, 1);
}
}