use std::collections::{BTreeMap, BTreeSet};
use serde::Serialize;
use crate::engine::CompiledRule;
use crate::error::RulesError;
use crate::recipe::SourceFile;
#[derive(Debug, Clone, Serialize)]
pub struct DataTable {
pub rule_id: String,
pub columns: Vec<String>,
pub rows: Vec<TableRow>,
pub summary: TableSummary,
}
#[derive(Debug, Clone, Serialize)]
pub struct TableRow {
pub path: String,
pub start_row: usize,
pub start_col: usize,
pub text: String,
pub bindings: BTreeMap<String, String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct TableSummary {
pub total_rows: usize,
pub files: usize,
pub per_file: BTreeMap<String, usize>,
}
impl DataTable {
pub fn to_json(&self) -> String {
serde_json::to_string(self).expect("DataTable serializes")
}
pub fn to_json_value(&self) -> serde_json::Value {
serde_json::to_value(self).expect("DataTable serializes")
}
}
pub fn data_table(rule: &CompiledRule, files: &[SourceFile]) -> Result<DataTable, RulesError> {
let mut rows: Vec<TableRow> = Vec::new();
let mut per_file: BTreeMap<String, usize> = BTreeMap::new();
for file in files {
if file.language != rule.language() {
continue;
}
let matches = rule.run(&file.source)?;
if matches.is_empty() {
continue;
}
let path = file.path.display().to_string();
per_file.insert(path.clone(), matches.len());
for m in matches {
rows.push(TableRow {
path: path.clone(),
start_row: m.span.start_row,
start_col: m.span.start_col,
text: m.text,
bindings: m
.bindings
.into_iter()
.map(|(name, binding)| (name, binding.text))
.collect(),
});
}
}
let columns: BTreeSet<String> = rows
.iter()
.flat_map(|r| r.bindings.keys().cloned())
.collect();
let summary = TableSummary {
total_rows: rows.len(),
files: per_file.len(),
per_file,
};
Ok(DataTable {
rule_id: rule.id().to_string(),
columns: columns.into_iter().collect(),
rows,
summary,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::Rule;
fn rule(toml: &str) -> CompiledRule {
CompiledRule::compile(&Rule::from_toml_str(toml).unwrap()).unwrap()
}
fn ts(path: &str, source: &str) -> SourceFile {
SourceFile::detect(path, source).unwrap()
}
#[test]
fn report_only_table_counts_sites_per_file() {
let rule = rule(
r#"
id = "find-calls"
language = "typescript"
[rule]
pattern = "$FN()"
"#,
);
let files = vec![
ts("a.ts", "foo();\nbar();\n"),
ts("b.ts", "baz();\n"),
ts("c.ts", "const x = 1;\n"),
];
let table = data_table(&rule, &files).unwrap();
assert_eq!(table.summary.total_rows, 3);
assert_eq!(table.summary.files, 2);
assert_eq!(table.summary.per_file["a.ts"], 2);
assert_eq!(table.summary.per_file["b.ts"], 1);
assert!(!table.summary.per_file.contains_key("c.ts"));
assert_eq!(table.columns, vec!["FN"]);
assert_eq!(table.rows[0].bindings["FN"], "foo");
}
#[test]
fn table_serializes_to_json() {
let rule = rule(
r#"
id = "r"
language = "typescript"
[rule]
pattern = "$FN()"
"#,
);
let table = data_table(&rule, &[ts("a.ts", "go();\n")]).unwrap();
let value = table.to_json_value();
assert_eq!(value["rule_id"], "r");
assert_eq!(value["summary"]["total_rows"], 1);
assert_eq!(value["rows"][0]["bindings"]["FN"], "go");
}
}