use std::fs;
use std::path::{Path, PathBuf};
use cure_engine::Config;
use cure_java_parser::parse;
use cure_java_print::print_unit;
use cure_java_simplify::simplify_unit;
const CORPUS_ROOT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/corpus_data/gjf");
const OBF_CORPUS_ROOT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/corpus_data/fernflower_obf");
const GJF_TESTDATA_ROOT: &str =
concat!(env!("CARGO_MANIFEST_DIR"), "/tests/corpus_data/gjf_testdata");
const CHECKSTYLE_ROOT: &str =
concat!(env!("CARGO_MANIFEST_DIR"), "/tests/corpus_data/checkstyle_noncompilable");
const OPENJDK_ROOT: &str =
concat!(env!("CARGO_MANIFEST_DIR"), "/tests/corpus_data/openjdk_langtools");
const CHECKSTYLE_GRAMMAR_ROOT: &str =
concat!(env!("CARGO_MANIFEST_DIR"), "/tests/corpus_data/checkstyle_grammar");
fn collect_java_files(root: &Path, out: &mut Vec<PathBuf>) {
let entries = match fs::read_dir(root) {
Ok(e) => e,
Err(_) => return,
};
for entry in entries.flatten() {
let p = entry.path();
if p.is_dir() {
let name = p.file_name().and_then(|n| n.to_str()).unwrap_or("");
if name == "target" || name == ".git" || name == "node_modules" {
continue;
}
collect_java_files(&p, out);
} else if p.extension().and_then(|e| e.to_str()) == Some("java") {
out.push(p);
}
}
}
fn run_corpus(root: &str, label: &str) {
if !Path::new(root).is_dir() {
eprintln!("skip corpus {label}: {root} not found");
return;
}
let mut files = Vec::new();
collect_java_files(Path::new(root), &mut files);
files.sort();
assert!(!files.is_empty(), "corpus {label} empty?");
let cfg = Config::default();
let mut clean = 0usize;
let mut dirty = 0usize;
let mut total_edits = 0usize;
let mut lines_before = 0usize;
let mut lines_after = 0usize;
let mut per_rule: std::collections::BTreeMap<&'static str, usize> = Default::default();
struct FileOutcome {
path: PathBuf,
was_clean: bool,
edits: usize,
by_rule: Vec<(&'static str, usize)>,
lines_before: usize,
lines_after: usize,
reparse_errs: Vec<(usize, String)>,
second_edits: usize,
second_by_rule: Vec<(&'static str, usize)>,
reparse_context: String,
}
let process = |f: &Path| -> Option<FileOutcome> {
let src = fs::read_to_string(f).ok()?; let mut outcome = parse(&src);
let was_clean = outcome.errors.is_empty();
let report = simplify_unit(&mut outcome.ast, &mut outcome.unit, &cfg);
let printed = print_unit(&outcome.ast, &outcome.unit);
if !was_clean {
eprintln!(" dirty: {}", f.display());
return Some(FileOutcome {
path: f.to_path_buf(),
was_clean,
edits: report.edits,
by_rule: report.by_rule.into_iter().collect(),
lines_before: count_code_lines(&src),
lines_after: count_code_lines(&printed),
reparse_errs: Vec::new(),
second_edits: 0,
second_by_rule: Vec::new(),
reparse_context: String::new(),
});
}
let reparsed = parse(&printed);
let reparse_errs: Vec<(usize, String)> = reparsed
.errors
.iter()
.take(3)
.map(|e| (e.line, e.message.clone()))
.collect();
let ctx = context_of(
&printed,
reparsed.errors.first().map(|e| e.line).unwrap_or(0),
);
let mut second = reparsed;
let r2 = simplify_unit(&mut second.ast, &mut second.unit, &cfg);
let final_edits;
let mut final_by_rule = r2.by_rule.clone();
if r2.edits > 0 {
let printed2 = print_unit(&second.ast, &second.unit);
let mut third = parse(&printed2);
let r3 = simplify_unit(&mut third.ast, &mut third.unit, &cfg);
final_edits = r3.edits;
final_by_rule = r3.by_rule.clone();
} else {
final_edits = 0;
}
Some(FileOutcome {
path: f.to_path_buf(),
was_clean,
edits: report.edits,
by_rule: report.by_rule.into_iter().collect(),
lines_before: count_code_lines(&src),
lines_after: count_code_lines(&printed),
reparse_errs,
second_edits: final_edits,
second_by_rule: final_by_rule.into_iter().collect(),
reparse_context: ctx,
})
};
let n_threads = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4)
.min(files.len())
.max(1);
let chunk = files.len().div_ceil(n_threads);
let mut outcomes: Vec<FileOutcome> = Vec::with_capacity(files.len());
std::thread::scope(|s| {
let handles: Vec<_> = files
.chunks(chunk)
.map(|c| s.spawn(move || c.iter().filter_map(|f| process(f)).collect::<Vec<_>>()))
.collect();
for h in handles {
outcomes.extend(h.join().expect("corpus 线程 panic"));
}
});
outcomes.sort_by(|a, b| a.path.cmp(&b.path));
for o in &outcomes {
total_edits += o.edits;
for (k, v) in &o.by_rule {
*per_rule.entry(k).or_insert(0) += v;
}
lines_before += o.lines_before;
lines_after += o.lines_after;
if o.was_clean {
clean += 1;
assert!(
o.reparse_errs.is_empty(),
"{}: 输出无法干净重新解析:{:?}\n== 附近输出 ==\n{}",
o.path.display(),
o.reparse_errs,
o.reparse_context
);
assert_eq!(
o.second_edits, 0,
"{}: 第三轮仍有 {} 次改写(全链路未收敛——疑似振荡/发散)\n{:?}",
o.path.display(),
o.second_edits,
o.second_by_rule
);
} else {
dirty += 1;
}
}
eprintln!(
"corpus[{label}]: {} 文件(干净 {} / 含语法错误 {}),{} 次改写,{} 行 → {} 行",
clean + dirty,
clean,
dirty,
total_edits,
lines_before,
lines_after
);
for (k, v) in &per_rule {
eprintln!(" rule {k}: {v}");
}
if total_edits > 0 {
eprintln!(
"平均每千行改写: {:.1}",
total_edits as f64 * 1000.0 / lines_before as f64
);
}
}
fn count_code_lines(src: &str) -> usize {
let mut out = 0usize;
let mut in_block = false;
for line in src.lines() {
let mut l = line.trim();
if in_block {
if let Some(i) = l.find("*/") {
in_block = false;
l = l[i + 2..].trim();
} else {
continue;
}
}
if l.starts_with("/*") && !l.ends_with("*/") {
in_block = true;
continue;
}
if l.starts_with("//") || l.starts_with("/*") || l.is_empty() {
continue;
}
out += 1;
}
out
}
fn context_of(text: &str, line: usize) -> String {
let mut out = String::new();
for (i, l) in text.lines().enumerate() {
if i + 1 >= line.saturating_sub(2) && i < line + 2 {
out.push_str(&format!("{:5}: {}\n", i + 1, l));
}
}
out
}
#[test]
fn all_vendored_corpora() {
let root = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/corpus_data"));
let mut dirs: Vec<PathBuf> = fs::read_dir(root)
.expect("corpus_data 不存在")
.flatten()
.map(|e| e.path())
.filter(|p| p.is_dir())
.collect();
dirs.sort();
assert!(!dirs.is_empty(), "corpus_data 无语料子目录");
for d in &dirs {
let label = d.file_name().and_then(|n| n.to_str()).unwrap_or("?");
run_corpus(d.to_str().unwrap(), label);
}
}