use anyhow::{Result, anyhow};
use regex::Regex;
use std::io::Read;
use std::sync::OnceLock;
use tree_sitter::{Node, Parser as TSParser};
use crate::code::language;
use crate::code::metadata;
use crate::code::{self, Code};
pub mod filesystem;
pub mod git;
pub mod license;
pub mod sampling;
static AUTO_GENERATED_RE: OnceLock<Regex> = OnceLock::new();
#[derive(Debug, Clone, Default)]
pub struct CodeStats {
pub code: Code,
pub automatically_generated: bool,
pub ast_nodes: usize,
pub lines_of_code: u64,
pub bytes: u64,
pub kind_stats: std::collections::HashMap<String, KindStats>,
pub failed_to_convert_to_utf8: bool,
pub failed_to_parse: bool,
pub too_large_to_parse: bool,
}
#[derive(Debug, Clone, Default)]
pub struct KindStats {
pub count: u64,
pub subtree_size_histogram: std::collections::HashMap<u32, u64>,
}
#[derive(Debug, Clone, Default)]
pub struct DiffStats {
pub commit_id: String,
pub relative_file_path: String,
pub before: Option<Code>,
pub after: Option<Code>,
pub git_reported_status: String,
pub bytes_before: u64,
pub bytes_after: u64,
pub lines_before: u64,
pub lines_after: u64,
pub nodes_before: u64,
pub nodes_after: u64,
pub unix_diff_script_bytes: u64,
pub lines_added: u64,
pub lines_removed: u64,
pub lines_changed: u64,
pub nodes_added: u64,
pub nodes_removed: u64,
pub nodes_changed: u64,
}
pub fn count_nodes(root: Node) -> usize {
let mut count = 0;
let mut stack = vec![root];
while let Some(node) = stack.pop() {
count += 1;
let mut cursor = node.walk();
stack.extend(node.children(&mut cursor));
}
count
}
pub fn compute_kind_stats(root: Node) -> (std::collections::HashMap<String, KindStats>, usize) {
let mut stats = std::collections::HashMap::new();
let total_nodes = visit_for_kind_stats(root, &mut stats);
(stats, total_nodes)
}
fn visit_for_kind_stats(
root: Node,
stats: &mut std::collections::HashMap<String, KindStats>,
) -> usize {
let mut order: Vec<(Node, Option<usize>)> = Vec::new();
let mut stack: Vec<(Node, Option<usize>)> = vec![(root, None)];
while let Some((node, parent)) = stack.pop() {
let index = order.len();
order.push((node, parent));
let mut cursor = node.walk();
stack.extend(node.children(&mut cursor).map(|child| (child, Some(index))));
}
let mut sizes = vec![1usize; order.len()];
for i in (0..order.len()).rev() {
let (node, parent) = order[i];
let size = sizes[i];
let entry = stats.entry(node.kind().to_string()).or_default();
entry.count += 1;
let bucket = size.ilog2();
*entry.subtree_size_histogram.entry(bucket).or_insert(0) += 1;
if let Some(parent) = parent {
sizes[parent] += size;
}
}
sizes[0]
}
pub fn is_generated(code: &str) -> bool {
let re = AUTO_GENERATED_RE.get_or_init(|| {
Regex::new(indoc::indoc!(r#"
(?imx)
^\s*
(?:(?://|/\*+|\#|;)\s*)? # optional comment prefix
.*? # anything on the line
(?:
@generated\b |
auto(?:matically)?\s+generated\b |
this\s+(?:file\s+)?(?:was|is)\s+(?:an?\s+)?auto(?:matically)?\s+generated(?:\s+file)?\b |
code\s+generated\s+by\b |
generated\s+by\b |
<auto-generated> |
do\s+not\s+edit(?:\s+this\s+file)?\b
)
"#))
.unwrap()
});
code.lines().take(50).any(|line| re.is_match(line))
}
const PARSE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
pub fn expand_from_code(stats: &mut CodeStats, parser: &mut TSParser) -> Result<()> {
match &stats.code.metadata.tip {
Some(tip) => match tip {
code::Type::Data(_) | code::Type::Documentation(_) => {
return Err(anyhow!(
"Can't compute statistics for non-code pretending to be code"
));
}
_ => {}
},
None => {
return Err(anyhow!(
"Can't compute statistics for code that has no type"
));
}
}
stats.bytes = stats.code.contents.len() as u64;
if let Some(language) = &stats.code.metadata.language {
if let Some(language) = language::to_treesitter(language) {
parser.set_language(&language)?;
if is_generated(&stats.code.contents) {
stats.automatically_generated = true;
return Ok(());
}
stats.lines_of_code = stats.code.contents.matches('\n').count() as u64;
let contents = stats.code.contents.as_bytes();
let started = std::time::Instant::now();
let mut give_up = |_: &tree_sitter::ParseState| started.elapsed() > PARSE_TIMEOUT;
let options = tree_sitter::ParseOptions::new().progress_callback(&mut give_up);
let parsed = parser.parse_with_options(
&mut |offset, _| &contents[offset.min(contents.len())..],
None,
Some(options),
);
match parsed {
Some(tree) => {
let (kind_stats, total_nodes) = compute_kind_stats(tree.root_node());
stats.ast_nodes = total_nodes;
stats.kind_stats = kind_stats;
}
None => {
stats.failed_to_parse = true;
eprintln!(
"Parse gave up after {}s: {:?}",
PARSE_TIMEOUT.as_secs(),
stats.code.metadata.path
);
}
}
}
} else {
return Err(anyhow!(
"Can't compute statistics for code that has no language"
));
};
Ok(())
}
pub fn for_path(path: &std::path::Path, parser: &mut TSParser) -> CodeStats {
let mut stats = CodeStats {
..Default::default()
};
stats.code.metadata.path = Some(std::path::PathBuf::from(path));
metadata::hermetic_expand(&mut stats.code.metadata);
if !matches!(
stats.code.metadata.tip,
Some(code::Type::Code(_)) | Some(code::Type::Configuration(_))
) {
return stats;
}
let mut f = match std::fs::File::open(path) {
Ok(f) => f,
Err(e) => {
eprintln!("Failed to open file {:?}: {:?}", path, e);
return stats;
}
};
if let Err(e) = f.read_to_string(&mut stats.code.contents) {
eprintln!("Failed to read file {:?}: {:?}", path, e);
return stats;
}
if let Err(e) = expand_from_code(&mut stats, parser) {
eprintln!("Failed to compute statistics: {:?}", e);
}
stats
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_generated_files() {
assert!(is_generated(
r"//
// This is an automatically generated file.
// Do not edit.
//
{
{0x00000000}, {0x33800000}, {0x34000000}, {0x34400000}
};"
));
assert!(!is_generated(""));
}
#[test]
fn deeply_nested_trees_are_walked_without_recursion() {
let depth = 50_000;
let source = format!("{}{}", "[".repeat(depth), "]".repeat(depth));
let code = crate::code::Code::from_string(&source, &crate::code::Language::JSON);
let root = code.ast.as_ref().expect("json parses").root_node();
let counted = count_nodes(root);
let (kinds, total) = compute_kind_stats(root);
assert_eq!(counted, total);
assert!(total >= 2 * depth, "{total} nodes for {depth} levels");
assert_eq!(kinds.values().map(|k| k.count).sum::<u64>(), total as u64);
}
#[test]
fn kind_stats_bucket_subtree_sizes_by_log2() {
let code = crate::code::Code::from_string("[1, 2]", &crate::code::Language::JSON);
let (kinds, _) = compute_kind_stats(code.ast.as_ref().unwrap().root_node());
let number = &kinds["number"];
assert_eq!(number.count, 2);
assert_eq!(number.subtree_size_histogram.get(&0), Some(&2));
let array = &kinds["array"];
assert_eq!(array.count, 1);
assert_eq!(array.subtree_size_histogram.get(&2), Some(&1));
}
#[test]
fn counts_lines_correctly() {
let mut stats = CodeStats {
code: Code {
contents: "line 1\nline 2\nline 3".to_string(),
metadata: crate::code::Metadata {
language: Some(crate::code::Language::Rust),
tip: Some(crate::code::Type::Code("rust".to_string())),
..Default::default()
},
..Default::default()
},
..Default::default()
};
let mut parser = tree_sitter::Parser::new();
expand_from_code(&mut stats, &mut parser).unwrap();
assert_eq!(stats.lines_of_code, 2);
}
}