use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use proc_macro2::Span;
use quote::ToTokens;
use syn::spanned::Spanned;
use syn::visit::Visit;
const DEFAULT_FN_MAX_LINES: usize = 200;
const EXCEPTIONS: &[(&str, &str, usize)] = &[
("bin/cargo_ktstr/kernel/mod.rs", "resolve_kernel_set", 210), ("bin/cargo_ktstr/stats.rs", "run_stats", 207), ("bin/jemalloc_alloc_worker.rs", "main", 206), ("bin/ktstr.rs", "write_show", 714), (
"cache/cache_dir_tests.rs",
"store_in_lock_recheck_mixed_content_peers_publish_one_per_group",
215,
), ("cli/kernel_build/build.rs", "kernel_build_pipeline", 481), ("ctprof_compare/compare.rs", "compare", 710), ("ctprof_compare/report/smaps.rs", "write_smaps_section", 212), ("ctprof_compare/runner.rs", "write_metric_list", 212), (
"ctprof_compare/tests_diff_types.rs",
"spec_thread_grouping_verbatim",
220,
), (
"ctprof_compare/tests_metrics.rs",
"registry_tag_matrix_is_pinned",
348,
), ("ctprof/mod.rs", "capture_with", 473), (
"ctprof/tests_capture.rs",
"capture_with_synthetic_tree_assembles_thread_state",
202,
), ("export.rs", "generate_preamble", 416), ("host_context.rs", "HostContext::diff", 226), ("host_thread_probe.rs", "find_jemalloc_via_maps_at", 240), ("monitor/btf_render/mod.rs", "chase_arena_pointer", 367), ("monitor/btf_render/mod.rs", "render_cast_pointer", 206), ("monitor/btf_render/mod.rs", "render_value_inner", 444), ("monitor/btf_render/mod.rs", "write_rendered_value", 332), ("monitor/btf_render/mod.rs", "write_struct", 276), (
"monitor/cast_analysis/mod.rs",
"Analyzer < 'a >::finalize",
414,
), (
"monitor/cast_analysis/mod.rs",
"Analyzer < 'a >::handle_ldx",
304,
), (
"monitor/cast_analysis/mod.rs",
"Analyzer < 'a >::handle_stx",
280,
), ("monitor/cast_analysis/mod.rs", "Analyzer < 'a >::run", 226), ("monitor/cast_analysis/mod.rs", "Analyzer < 'a >::step", 391), ("monitor/cast_analysis/tests.rs", "build_btf", 223), (
"monitor/cast_analysis/tests.rs",
"helper_map_update_then_lookup_propagates_arena_through_map_value",
220,
), (
"monitor/dump/display.rs",
"<FailureDumpReport as std :: fmt :: Display>::fmt",
240,
), ("monitor/dump/mod.rs", "dump_state", 1146), ("monitor/dump/render_map.rs", "render_map", 551), ("monitor/mod.rs", "MonitorThresholds::evaluate", 211), ("monitor/reader.rs", "monitor_loop", 808), ("probe/btf.rs", "parse_bpf_btf_functions", 205), ("probe/output.rs", "format_probe_events_inner", 330), ("probe/process.rs", "attach_phase_b_fentry", 494), ("probe/process.rs", "run_probe_skeleton", 1280), ("scenario/ops/mod.rs", "apply_ops", 716), ("scenario/ops/mod.rs", "apply_setup", 507), ("scenario/ops/mod.rs", "run_scenario", 300), ("stats.rs", "compare_partitions", 242), ("stats.rs", "group_and_average_by", 257), (
"taskstats.rs",
"parse_taskstats_payload_version_boundary_truncation",
249,
), ("test_support/entry.rs", "KtstrTestEntry::validate", 251), ("test_support/eval.rs", "evaluate_vm_result", 530), ("test_support/eval.rs", "run_ktstr_test_inner_impl", 898), ("test_support/probe.rs", "attempt_auto_repro", 390), (
"test_support/probe.rs",
"maybe_dispatch_vm_test_with_args",
215,
), (
"test_support/probe.rs",
"maybe_dispatch_vm_test_with_phase_a",
247,
), ("topology.rs", "TestTopology::from_system", 218), ("vmm/builder.rs", "KtstrVmBuilder::build", 244), (
"vmm/cast_analysis_load/mod.rs",
"analyze_one_object_with_btf",
217,
), ("vmm/cgroup_sandbox.rs", "BuildSandbox::try_create", 235), ("vmm/freeze_coord/dispatch.rs", "dispatch_bulk_message", 253), ("vmm/freeze_coord/mod.rs", "KtstrVm::collect_results", 419), ("vmm/freeze_coord/mod.rs", "KtstrVm::run_bsp_loop", 225), ("vmm/freeze_coord/mod.rs", "KtstrVm::run_vm", 7718), (
"vmm/freeze_coord/mod.rs",
"KtstrVm::start_bpf_map_write",
202,
), ("vmm/freeze_coord/mod.rs", "KtstrVm::start_monitor", 780), ("vmm/initramfs.rs", "build_initramfs_base", 277), ("vmm/mod.rs", "KtstrVm::run_interactive", 626), ("vmm/rust_init.rs", "ktstr_guest_init", 616), ("vmm/rust_init.rs", "run_relay_session", 209), ("vmm/rust_init.rs", "start_sched_exit_monitor", 222), ("vmm/sched_stats.rs", "SchedStatsClient::request_raw", 230), ("vmm/setup.rs", "KtstrVm::init_virtio_blk", 205), ("vmm/setup.rs", "KtstrVm::setup_memory", 227), (
"vmm/virtio_blk/device.rs",
"VirtioBlk::handle_read_vectored_impl",
204,
), ("vmm/virtio_blk/device.rs", "VirtioBlk::set_status", 420), ("vmm/virtio_blk/drain.rs", "drain_bracket_impl", 1197), ("vmm/virtio_blk/worker.rs", "worker_thread_main", 515), (
"vmm/virtio_net/device.rs",
"VirtioNet::process_tx_loopback",
278,
), (
"vmm/virtio_net/device.rs",
"VirtioNet::try_loopback_to_rx",
376,
), ("vmm/x86_64/kvm.rs", "KtstrKvm::new_inner", 261), ("workload/spawn/mod.rs", "spawn_pcomm_container", 781), ("workload/spawn/mod.rs", "WorkloadHandle::spawn", 232), ("workload/spawn/mod.rs", "WorkloadHandle::spawn_group", 959), (
"workload/spawn/mod.rs",
"WorkloadHandle::spawn_pcomm_cgroup",
271,
), (
"workload/spawn/mod.rs",
"WorkloadHandle::stop_and_collect",
483,
), ("workload/worker/mod.rs", "worker_main", 3209), ];
fn src_root() -> PathBuf {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect(
"CARGO_MANIFEST_DIR must be set (cargo sets it for cargo-test / cargo-nextest \
invocations; running this test outside of cargo is unsupported)",
);
PathBuf::from(manifest_dir).join("src")
}
fn rel_path(file: &Path, src_root: &Path) -> String {
let rel = file
.strip_prefix(src_root)
.expect("file must live under src_root");
rel.components()
.map(|c| c.as_os_str().to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join("/")
}
struct FnVisitor {
impl_ctx: Option<String>,
out: Vec<(String, usize)>,
}
impl FnVisitor {
fn new() -> Self {
Self {
impl_ctx: None,
out: Vec::new(),
}
}
fn record(&mut self, fn_name: &str, body_span: Span) {
let start = body_span.start().line;
let end = body_span.end().line;
let length = end.saturating_sub(start).saturating_add(1);
let key = match &self.impl_ctx {
Some(receiver) if receiver.contains(" as ") => {
format!("<{receiver}>::{fn_name}")
}
Some(receiver) => format!("{receiver}::{fn_name}"),
None => fn_name.to_string(),
};
self.out.push((key, length));
}
}
fn render_tokens<T: ToTokens>(value: &T) -> String {
let s = value.to_token_stream().to_string();
let mut compact = String::with_capacity(s.len());
let mut last_space = true;
for c in s.chars() {
if c.is_whitespace() {
if !last_space {
compact.push(' ');
last_space = true;
}
} else {
compact.push(c);
last_space = false;
}
}
compact.trim().to_string()
}
impl<'ast> Visit<'ast> for FnVisitor {
fn visit_item_fn(&mut self, node: &'ast syn::ItemFn) {
self.record(
&node.sig.ident.to_string(),
node.block.brace_token.span.span(),
);
syn::visit::visit_item_fn(self, node);
}
fn visit_item_impl(&mut self, node: &'ast syn::ItemImpl) {
let receiver = render_tokens(&node.self_ty);
let saved = self.impl_ctx.take();
self.impl_ctx = Some(match &node.trait_ {
Some((_, trait_path, _)) => {
let trait_s = render_tokens(trait_path);
format!("{receiver} as {trait_s}")
}
None => receiver,
});
syn::visit::visit_item_impl(self, node);
self.impl_ctx = saved;
}
fn visit_impl_item_fn(&mut self, node: &'ast syn::ImplItemFn) {
self.record(
&node.sig.ident.to_string(),
node.block.brace_token.span.span(),
);
syn::visit::visit_impl_item_fn(self, node);
}
fn visit_trait_item_fn(&mut self, node: &'ast syn::TraitItemFn) {
if let Some(block) = &node.default {
self.record(&node.sig.ident.to_string(), block.brace_token.span.span());
}
syn::visit::visit_trait_item_fn(self, node);
}
}
fn collect_functions(file: &Path) -> Vec<(String, usize)> {
let source = std::fs::read_to_string(file).expect("read source file");
let parsed = match syn::parse_file(&source) {
Ok(p) => p,
Err(e) => {
eprintln!(
"[src_function_size_guard] warning: skipped {} (syn parse failed: {e})",
file.display()
);
return Vec::new();
}
};
let mut v = FnVisitor::new();
v.visit_file(&parsed);
v.out
}
#[test]
#[ignore]
fn no_src_function_exceeds_200_lines_unless_grandfathered() {
let src = src_root();
assert!(
src.is_dir(),
"src directory does not exist at {src:?}; CARGO_MANIFEST_DIR may be wrong",
);
let exceptions: BTreeMap<(String, String), usize> = EXCEPTIONS
.iter()
.map(|(f, n, c)| ((f.to_string(), n.to_string()), *c))
.collect();
assert_eq!(
exceptions.len(),
EXCEPTIONS.len(),
"EXCEPTIONS contains a duplicate (file, fn_name) key — each entry must \
appear exactly once",
);
let mut new_overflows: Vec<(String, String, usize)> = Vec::new();
let mut grew_past_ceiling: Vec<(String, String, usize, usize)> = Vec::new();
let mut seen_exceptions: BTreeMap<(String, String), bool> =
exceptions.keys().map(|k| (k.clone(), false)).collect();
for entry in walkdir::WalkDir::new(&src).into_iter() {
let entry = entry.expect("walkdir must succeed under src/");
let path = entry.path();
if !entry.file_type().is_file() {
continue;
}
if path.extension().and_then(|e| e.to_str()) != Some("rs") {
continue;
}
let rel = rel_path(path, &src);
let fns = collect_functions(path);
for (name, length) in fns {
if length <= DEFAULT_FN_MAX_LINES {
continue;
}
let key = (rel.clone(), name);
match exceptions.get(&key) {
Some(&ceiling) => {
if let Some(seen) = seen_exceptions.get_mut(&key) {
*seen = true;
}
if length > ceiling {
let (file, fn_name) = key;
grew_past_ceiling.push((file, fn_name, length, ceiling));
}
}
None => {
let (file, fn_name) = key;
new_overflows.push((file, fn_name, length));
}
}
}
}
let stale_exceptions: Vec<(String, String)> = seen_exceptions
.iter()
.filter_map(|(k, seen)| if *seen { None } else { Some(k.clone()) })
.collect();
let any_failure =
!new_overflows.is_empty() || !grew_past_ceiling.is_empty() || !stale_exceptions.is_empty();
if any_failure {
let mut msg = String::from("src-function size guard failed:\n\n");
if !new_overflows.is_empty() {
msg.push_str("(1) Functions NOT in EXCEPTIONS that exceed 200 lines:\n");
for (file, name, length) in &new_overflows {
msg.push_str(&format!(
" (\"{file}\", \"{name}\", {length}), // queued: decompose\n"
));
}
msg.push_str(
" Fix: decompose the function into helpers, or add the entry \
to EXCEPTIONS with its current line count and a `// queued: \
<task>` comment naming the queued decomposition task.\n\n",
);
}
if !grew_past_ceiling.is_empty() {
msg.push_str("(2) Grandfathered functions that grew past their pinned ceiling:\n");
for (file, name, length, ceiling) in &grew_past_ceiling {
msg.push_str(&format!(
" src/{file}::{name} = {length} lines (grandfathered ceiling {ceiling})\n"
));
}
msg.push_str(
" Fix: decompose (preferred) OR refresh the EXCEPTIONS entry's \
count if the growth is genuinely unavoidable.\n\n",
);
}
if !stale_exceptions.is_empty() {
msg.push_str(
"(3) EXCEPTIONS entries that are now stale (function ≤ 200 \
lines or removed) — remove these entries:\n",
);
for (file, name) in &stale_exceptions {
msg.push_str(&format!(" (\"{file}\", \"{name}\", _)\n"));
}
msg.push_str(
" Fix: delete the listed entries from EXCEPTIONS. The \
default 200-line gate guards the function going forward.\n\n",
);
}
panic!("{msg}");
}
}