Skip to main content

bvr/
lib.rs

1#![forbid(unsafe_code)]
2#![allow(clippy::cast_lossless)]
3#![allow(clippy::cast_precision_loss)]
4#![allow(clippy::implicit_hasher)]
5#![allow(clippy::missing_const_for_fn)]
6#![allow(clippy::missing_errors_doc)]
7#![allow(clippy::must_use_candidate)]
8#![allow(clippy::needless_pass_by_value)]
9#![allow(clippy::struct_excessive_bools)]
10#![allow(clippy::suboptimal_flops)]
11#![allow(clippy::too_many_lines)]
12
13pub mod agents;
14pub mod analysis;
15pub mod cli;
16pub mod error;
17pub mod export_md;
18pub mod export_pages;
19pub mod export_sqlite;
20pub mod loader;
21pub mod model;
22pub mod pages_wizard;
23pub mod robot;
24pub mod tui;
25pub mod viewer_assets;
26
27pub use error::{BvrError, Result};
28
29#[cfg(test)]
30mod version_guard {
31    //! Catches hard-coded version strings that should use `env!("CARGO_PKG_VERSION")`.
32    //!
33    //! When adding a new struct with a `version` or `schema_version` field, use
34    //! `env!("CARGO_PKG_VERSION")` instead of a string literal. This test will
35    //! fail if a literal version string sneaks in.
36
37    use std::path::Path;
38
39    /// Walk `src/` looking for version string literals in non-test production code.
40    #[test]
41    fn no_hardcoded_version_strings_in_production_code() {
42        let src_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
43        let pkg_version = env!("CARGO_PKG_VERSION");
44
45        // Patterns that indicate a hard-coded version where env!() should be used.
46        let suspicious = [
47            format!("\"{}\"", pkg_version), // e.g. "0.1.0"
48            "\"1.0.0\"".to_string(),
49            "\"1.0\"".to_string(),
50            "\"2.0.0\"".to_string(),
51            "\"2.0\"".to_string(),
52        ];
53
54        // Allowlisted files/patterns (test code, HTML markers, HTTP headers, etc.)
55        let allowlist: &[&str] = &[
56            "agents.rs", // AGENT_BLURB HTML markers — guarded by agent_blurb_version_matches_constant
57            "viewer_assets.rs", // Embedded asset content
58        ];
59
60        let mut violations = Vec::new();
61
62        for entry in walkdir(src_dir) {
63            let path = entry.as_path();
64            if !path.extension().is_some_and(|ext| ext == "rs") {
65                continue;
66            }
67
68            let filename = path.file_name().unwrap_or_default().to_string_lossy();
69            if allowlist.iter().any(|a| filename.as_ref() == *a) {
70                continue;
71            }
72
73            let content = match std::fs::read_to_string(path) {
74                Ok(c) => c,
75                Err(_) => continue,
76            };
77
78            // Split into production vs test code at #[cfg(test)]
79            let prod_code = content.split("#[cfg(test)]").next().unwrap_or(&content);
80
81            for pattern in &suspicious {
82                for (line_no, line) in prod_code.lines().enumerate() {
83                    let trimmed = line.trim();
84                    // Skip comments
85                    if trimmed.starts_with("//") || trimmed.starts_with("///") {
86                        continue;
87                    }
88                    if line.contains(pattern.as_str()) {
89                        violations.push(format!(
90                            "{}:{}: contains {} — use env!(\"CARGO_PKG_VERSION\") instead",
91                            path.display(),
92                            line_no + 1,
93                            pattern,
94                        ));
95                    }
96                }
97            }
98        }
99
100        assert!(
101            violations.is_empty(),
102            "Found hard-coded version strings in production code:\n{}",
103            violations.join("\n")
104        );
105    }
106
107    /// Simple recursive directory walker (no external dependency needed).
108    fn walkdir(dir: std::path::PathBuf) -> Vec<std::path::PathBuf> {
109        let mut files = Vec::new();
110        if let Ok(entries) = std::fs::read_dir(&dir) {
111            for entry in entries.flatten() {
112                let path = entry.path();
113                if path.is_dir() {
114                    files.extend(walkdir(path));
115                } else {
116                    files.push(path);
117                }
118            }
119        }
120        files
121    }
122}