use std::path::{Path, PathBuf};
const FORBIDDEN: &[&str] = &[
"DarkGray",
"Color::Gray",
".gray()",
".dark_gray()",
"Color::Indexed(7)",
"Color::Indexed(8)",
"Color::White",
".white()",
"Color::Black",
".black()",
];
const BORDER_MARKERS: &[&str] = &["border_style", "Borders::", ".border", "border:"];
fn rs_files(dir: &Path, out: &mut Vec<PathBuf>) {
for entry in std::fs::read_dir(dir).unwrap() {
let p = entry.unwrap().path();
if p.is_dir() {
rs_files(&p, out);
} else if p.extension().is_some_and(|e| e == "rs") {
out.push(p);
}
}
}
fn scanned_files() -> Vec<PathBuf> {
let cli = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/cli");
let mut files = Vec::new();
rs_files(&cli.join("tui"), &mut files);
rs_files(&cli.join("monitor"), &mut files);
files.sort();
files
}
fn violations(file: &str, src: &str) -> Vec<String> {
src.lines()
.enumerate()
.filter(|(_, l)| FORBIDDEN.iter().any(|t| l.contains(t)))
.filter(|(_, l)| !BORDER_MARKERS.iter().any(|m| l.contains(m)))
.map(|(i, l)| format!("{file}:{}: {}", i + 1, l.trim()))
.collect()
}
#[test]
fn text_never_uses_gray_white_black_or_bright_black() {
let mut found = Vec::new();
for path in scanned_files() {
let src = std::fs::read_to_string(&path).unwrap();
found.extend(violations(&path.to_string_lossy(), &src));
}
assert!(
found.is_empty(),
"use default foreground + Modifier::DIM for secondary text; DarkGray only on border styles:\n{}",
found.join("\n")
);
}
#[test]
fn scanner_flags_text_and_spares_only_border_styles() {
let src = [
"a.fg(Color::DarkGray)", "b.border_style(Style::new().fg(Color::DarkGray))", "c.gray()", "d.dark_gray()", "e.fg(Color::Gray)", "f.fg(Color::LightGray)", "g.fg(Color::Indexed(7))", "h.fg(Color::Indexed(8))", "i.fg(Color::White)", "j.white()", "k.fg(Color::Black)", "l.black()", "m.fg(Color::Indexed(9))", "let border = Style::new().gray(); // not a border call", "let x = Borders::ALL; y.fg(Color::DarkGray)", "let bordered_text = Style::new().fg(Color::White);", "border: Color::DarkGray,", ]
.join("\n");
let lines: Vec<usize> = violations("x.rs", &src)
.iter()
.map(|v| v.split(':').nth(1).unwrap().parse().unwrap())
.collect();
assert_eq!(lines, vec![1, 3, 4, 5, 7, 8, 9, 10, 11, 12, 14, 16]);
}
const COLOR_TOKENS: &[&str] = &[
"Color::",
".red()",
".cyan()",
".yellow()",
".green()",
".blue()",
".magenta()",
".bright_",
"OwoColorize",
"use colored::",
];
fn sets_color_sgr(line: &str) -> bool {
["\\x1b[", "\\u{1b}["].iter().any(|open| {
line.match_indices(open).any(|(at, _)| {
let rest = &line[at + open.len()..];
let params = rest.split('m').next().unwrap_or("");
params
.split(';')
.filter_map(|p| p.parse::<u16>().ok())
.any(|n| (30..=49).contains(&n) || (90..=107).contains(&n))
})
})
}
fn imports_color(stmt: &str) -> bool {
let Some((_, path)) = stmt.split_once("style::") else {
return false;
};
let names = path.trim_start_matches('{');
names
.split([',', '}', ';'])
.map(str::trim)
.any(|name| name == "Color" || name.starts_with("Color as "))
}
fn is_test_module(line: &str) -> bool {
let rest = line.trim_start();
let rest = rest
.strip_prefix("pub(crate) ")
.or_else(|| rest.strip_prefix("pub "))
.unwrap_or(rest);
rest.strip_prefix("mod tests")
.is_some_and(|after| after.is_empty() || after.starts_with([' ', '{', ';']))
}
fn color_uses(file: &str, src: &str) -> Vec<String> {
let lines: Vec<&str> = src.lines().collect();
let end = lines
.windows(2)
.position(|w| w[0].trim() == "#[cfg(test)]" && is_test_module(w[1]))
.unwrap_or(lines.len());
let mut found = Vec::new();
let mut i = 0;
while i < end {
let line = lines[i];
let flag = |found: &mut Vec<String>| {
found.push(format!("{file}:{}: {}", i + 1, line.trim()));
};
if line.trim_start().starts_with("use ") {
let last = (i..end)
.find(|&j| lines[j].contains(';'))
.unwrap_or(end - 1);
let stmt = lines[i..=last].join(" ");
if imports_color(&stmt) || COLOR_TOKENS.iter().any(|t| stmt.contains(t)) {
flag(&mut found);
}
i = last + 1;
continue;
}
if COLOR_TOKENS.iter().any(|t| line.contains(t)) || sets_color_sgr(line) {
flag(&mut found);
}
i += 1;
}
found
}
fn is_theme_file(path: &Path) -> bool {
let p = path.to_string_lossy().replace('\\', "/");
p.contains("scope-core/src/display/theme/") || p.ends_with("scope-cli/src/cli/theme.rs")
}
fn is_test_file(path: &Path) -> bool {
path.file_name()
.is_some_and(|n| n.to_string_lossy().ends_with("tests.rs"))
}
#[test]
fn no_crate_names_a_color_outside_the_theme() {
let crates = Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
let mut files = Vec::new();
for krate in ["scope-core", "scope-cli", "scope-tui-harness"] {
rs_files(&crates.join(krate).join("src"), &mut files);
}
files.sort();
let mut found = Vec::new();
for path in files
.iter()
.filter(|p| !is_theme_file(p) && !is_test_file(p))
{
let src = std::fs::read_to_string(path).unwrap();
found.extend(color_uses(&path.to_string_lossy(), &src));
}
assert!(
found.is_empty(),
"name colors only in scope::display::theme or cli/theme.rs (style, strong, ACCENT, KEY, ...):\n{}",
found.join("\n")
);
}
#[test]
fn sgr_scanner_flags_colors_and_spares_effects() {
assert!(sets_color_sgr(r"x \x1b[31mred\x1b[0m"));
assert!(sets_color_sgr(r"x \x1b[1;35mkey"));
assert!(sets_color_sgr(r"x \u{1b}[92mbright"));
assert!(sets_color_sgr(r"x \x1b[38;5;208m"));
assert!(!sets_color_sgr(r"x \x1b[1mExamples:\x1b[0m"));
assert!(!sets_color_sgr(r"x \x1b[2m# note\x1b[0m"));
assert!(!sets_color_sgr(r"x \x1b[7mreverse"));
}
#[test]
fn a_visible_test_module_also_ends_the_scan() {
let src = "#[cfg(test)]\npub(crate) mod tests {\na.fg(Color::Red)\n}";
assert!(color_uses("x.rs", src).is_empty());
assert!(!is_test_module("mod testsupport_helpers"));
}
#[test]
fn color_scanner_flags_named_colors_and_shorthands_in_non_test_code() {
let src = [
"a.fg(Color::Red)", "b.red()", "c.cyan().bold()", "d.yellow()", "e.green()", "f.blue()", "g.magenta()", "h.bold()", "i.fg(ACCENT.fg)", "#[cfg(test)]", "static LOCK: () = ();", "k.fg(Color::Red)", "use ratatui::style::Color;", "use ratatui::style::{Color, Modifier};", "use ratatui::style::{Modifier, Style};", "use ratatui::style::{", " Color as C,", "};", "use crate::cli::theme::{ACCENT, KEY};", "#[cfg(test)]", "mod tests {", "j.fg(Color::Red)", "use ratatui::style::Color;", ]
.join("\n");
let lines: Vec<usize> = color_uses("x.rs", &src)
.iter()
.map(|v| v.split(':').nth(1).unwrap().parse().unwrap())
.collect();
assert_eq!(lines, vec![1, 2, 3, 4, 5, 6, 7, 12, 13, 14, 16]);
}