use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use blue_lang_check::{Code, RULES};
use blue_lang_runtime::pipeline::{check_entry, render, Checking};
use blue_lang_runtime::uses::Entry;
use blue_lang_runtime::word_shapes::Standing;
pub struct Census {
pub files: usize,
pub findings: BTreeMap<Code, BTreeSet<String>>,
}
impl Census {
#[must_use]
pub fn mismatches(&self) -> Vec<(Code, usize, u32)> {
RULES
.iter()
.filter_map(|r| {
let ratchet = r.ratchet?;
let measured = self.findings.get(&r.code).map_or(0, BTreeSet::len);
(measured != ratchet as usize).then_some((r.code, measured, ratchet))
})
.collect()
}
}
pub fn take(root: &Path) -> Result<Census, String> {
let files = blue_lang_pkg::corpus::files(root);
let loader = blue_lang_pkg::corpus::load_path(root, &files);
let mut findings: BTreeMap<Code, BTreeSet<String>> = BTreeMap::new();
for path in &files {
let text = std::fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()))?;
let checked = check_entry(
Entry {
path: Some(path),
text: &text,
},
&loader,
None,
Checking::WithTests,
)
.map_err(|e| format!("{}: {e}", path.display()))?;
let ratcheted = |d: &&blue_lang_check::Diagnostic| d.code.rule().ratchet.is_some();
for d in checked
.outcome
.census
.iter()
.chain(checked.outcome.diagnostics.iter().filter(ratcheted))
{
findings
.entry(d.code)
.or_default()
.insert(render(&checked.program, d));
}
}
Ok(Census {
files: files.len(),
findings,
})
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ReuseFact {
pub scope: String,
pub file: String,
pub line: usize,
pub name: String,
pub shape: String,
pub size: usize,
pub home: Option<String>,
pub fix: Option<&'static str>,
pub word: bool,
pub waived: bool,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ReuseScan {
pub files: usize,
pub definitions: usize,
pub facts: Vec<ReuseFact>,
pub blind: Vec<(String, String)>,
}
impl ReuseScan {
#[must_use]
pub fn to_json(&self) -> serde_json::Value {
serde_json::json!({
"files": self.files,
"definitions": self.definitions,
"blind": self.blind.iter().map(|(file, why)| serde_json::json!({
"file": file,
"why": why,
})).collect::<Vec<_>>(),
"facts": self.facts.iter().map(ReuseFact::to_json).collect::<Vec<_>>(),
})
}
}
impl ReuseFact {
#[must_use]
pub fn to_json(&self) -> serde_json::Value {
serde_json::json!({
"scope": self.scope,
"file": self.file,
"line": self.line,
"name": self.name,
"shape": self.shape,
"size": self.size,
"home": self.home,
"fix": self.fix,
"word": self.word,
"waived": self.waived,
})
}
}
fn package_roots(files: &[std::path::PathBuf]) -> Vec<std::path::PathBuf> {
let mut roots: Vec<std::path::PathBuf> = files
.iter()
.filter_map(|f| {
let dir = f.parent()?;
(dir.file_name()? == f.file_stem()?).then(|| dir.parent().map(Path::to_path_buf))?
})
.collect();
roots.sort();
roots.dedup();
roots
}
pub(crate) fn environment_over(
files: &[std::path::PathBuf],
) -> blue_lang_pkg::load_path::Environment {
let mut roots = package_roots(files);
roots.extend(
std::env::var_os(blue_lang_pkg::load_path::BLUE_PATH)
.as_deref()
.map_or_else(Vec::new, |v| std::env::split_paths(v).collect()),
);
blue_lang_pkg::load_path::Environment::from_vars(
std::env::join_paths(roots).unwrap_or_default().as_os_str(),
std::env::var_os(blue_lang_pkg::load_path::BLUE_DIST)
.unwrap_or_default()
.as_os_str(),
)
}
#[must_use]
pub fn reuse_scan(root: &Path) -> ReuseScan {
let files = blue_lang_pkg::corpus::files(root);
let mut scan = ReuseScan {
files: files.len(),
..ReuseScan::default()
};
let floor = blue_lang_check::shape::Near::DEFAULT.min_size;
let environment = environment_over(&files);
for path in &files {
let rel = path
.strip_prefix(root)
.unwrap_or(path)
.display()
.to_string();
let text = match std::fs::read_to_string(path) {
Ok(t) => t,
Err(e) => {
scan.blind.push((rel, e.to_string()));
continue;
}
};
let loader = environment.load_path(Some(path));
let checked = match check_entry(
Entry {
path: Some(path),
text: &text,
},
&loader,
None,
Checking::WithTests,
) {
Ok(c) => c,
Err(e) => {
scan.blind.push((rel, e.to_string()));
continue;
}
};
let package = checked
.program
.file(blue_lang_runtime::uses::ResolvedProgram::ENTRY)
.and_then(|f| f.package.clone());
let scope = package.unwrap_or_else(|| format!("%root:{rel}"));
let shapes = checked.entry_shapes();
scan.definitions += shapes.len();
for s in shapes.iter().filter(|s| s.size >= floor) {
let start = checked.program.forms()[s.top_level].span.start;
let line = text
.get(..start)
.map_or(1, |head| head.matches('\n').count() + 1);
let standing = blue_lang_runtime::word_shapes::standing(&s.hash, &s.key);
let (home, fix, word) = match standing {
Standing::NoWord => (None, None, false),
Standing::OwnWord => (None, None, true),
Standing::CopyOf { word, foundation } => (
Some(word.to_string()),
Some(if foundation { "import" } else { "move_down" }),
false,
),
};
let waived = checked
.outcome
.waived
.iter()
.any(|w| w.waiver.code == Code::B0070 && w.waiver.top_level == s.top_level);
scan.facts.push(ReuseFact {
scope: scope.clone(),
file: rel.clone(),
line,
name: s.name().to_string(),
shape: s.hash.hex(),
size: s.size,
home,
fix,
word,
waived,
});
}
}
scan
}
#[must_use]
pub fn word_copy_lines(file: &Path) -> Vec<String> {
let Ok(text) = std::fs::read_to_string(file) else {
return Vec::new();
};
let loader = blue_lang_pkg::load_path::LoadPath::for_entry(file);
let found = blue_lang_runtime::pipeline::word_copies(
Entry {
path: Some(file),
text: &text,
},
&loader,
);
found
.unwrap_or_default()
.iter()
.map(|d| {
let (line, column) = blue_lang_syntax::Span::line_col(&text, d.span.start);
format!(
"{}:{line}:{column}: {}: {}",
file.display(),
d.message,
d.help.as_deref().unwrap_or_default()
)
})
.collect()
}
fn hook_file(input: &str) -> Option<std::path::PathBuf> {
let doc: serde_json::Value = serde_json::from_str(input).ok()?;
let path = Path::new(doc.get("tool_input")?.get("file_path")?.as_str()?);
(path.extension().is_some_and(|x| x == "b") && path.is_file()).then(|| path.to_path_buf())
}
pub fn reuse_hook() {
use std::io::{Read, Write};
std::panic::set_hook(Box::new(|_| {}));
let _ = std::panic::catch_unwind(|| {
let mut input = String::new();
let _ = std::io::stdin().read_to_string(&mut input);
let Some(file) = hook_file(&input) else {
return;
};
let mut out = std::io::stdout().lock();
for line in word_copy_lines(&file) {
let _ = writeln!(out, "{line}");
}
});
}