oxedyne/fe2o3/fe2o3_text/src/bin/annealer.rs
7.3 KiB, 41 runs
created by r1870400018:11647, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! Annealer -- the Oxedyne code formatter. |
| 2 | //! |
| 3 | //! CLI binary for formatting source files using the Wadler/Lindig |
| 4 | //! layout algebra engine in `fe2o3_text::fmt`. |
| 5 | //! |
| 6 | //! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\ |
| 7 | //! Anthropic Claude |
| 8 | #![forbid(unsafe_code)] |
| 9 | |
| 10 | use oxedyne_fe2o3_core::ok; |
| 11 | use oxedyne_fe2o3_text::fmt::{ |
| 12 | self, |
| 13 | spec::FormatSpec, |
| 14 | }; |
| 15 | |
| 16 | use std::io::Read; |
| 17 | use std::path::Path; |
| 18 | use std::process; |
| 19 | |
| 20 | |
| 21 | const LANGS: &[&str] = &["rust", "c", "cpp", "csharp", "go", "java", "javascript", "python"]; |
| 22 | |
| 23 | fn main() { |
| 24 | let args: Vec<String> = std::env::args().skip(1).collect(); |
| 25 | |
| 26 | if args.is_empty() || args.iter().any(|a| a == "--help" || a == "-h") { |
| 27 | print_usage(); |
| 28 | process::exit(0); |
| 29 | } |
| 30 | |
| 31 | let mut write = false; |
| 32 | let mut check = false; |
| 33 | let mut stdin = false; |
| 34 | let mut lang: Option<String> = None; |
| 35 | let mut config: Option<String> = None; |
| 36 | let mut paths: Vec<String> = Vec::new(); |
| 37 | |
| 38 | let mut i = 0; |
| 39 | while i < args.len() { |
| 40 | match args[i].as_str() { |
| 41 | "--write" | "-w" => write = true, |
| 42 | "--check" | "-c" => check = true, |
| 43 | "--stdin" => stdin = true, |
| 44 | "--lang" => { |
| 45 | i += 1; |
| 46 | if i >= args.len() { |
| 47 | eprintln!("error: --lang requires a value"); |
| 48 | process::exit(2); |
| 49 | } |
| 50 | lang = Some(args[i].to_lowercase()); |
| 51 | } |
| 52 | "--config" => { |
| 53 | i += 1; |
| 54 | if i >= args.len() { |
| 55 | eprintln!("error: --config requires a path"); |
| 56 | process::exit(2); |
| 57 | } |
| 58 | config = Some(args[i].clone()); |
| 59 | } |
| 60 | other if other.starts_with('-') => { |
| 61 | eprintln!("error: unknown flag '{}'", other); |
| 62 | process::exit(2); |
| 63 | } |
| 64 | _ => paths.push(args[i].clone()), |
| 65 | } |
| 66 | i += 1; |
| 67 | } |
| 68 | |
| 69 | if write && check { |
| 70 | eprintln!("error: --write and --check are mutually exclusive"); |
| 71 | process::exit(2); |
| 72 | } |
| 73 | |
| 74 | // Load format specification. |
| 75 | let spec = match config { |
| 76 | Some(ref path) => { |
| 77 | let content = match std::fs::read_to_string(path) { |
| 78 | Ok(s) => s, |
| 79 | Err(e) => { |
| 80 | eprintln!("error: cannot read config '{}': {}", path, e); |
| 81 | process::exit(1); |
| 82 | } |
| 83 | }; |
| 84 | match FormatSpec::from_config_str(&content) { |
| 85 | Ok(s) => s, |
| 86 | Err(e) => { |
| 87 | eprintln!("error: invalid config '{}': {}", path, e); |
| 88 | process::exit(1); |
| 89 | } |
| 90 | } |
| 91 | } |
| 92 | None => FormatSpec::fe2o3(), |
| 93 | }; |
| 94 | |
| 95 | // Stdin mode. |
| 96 | if stdin { |
| 97 | let mut src = String::new(); |
| 98 | if let Err(e) = std::io::stdin().read_to_string(&mut src) { |
| 99 | eprintln!("error: failed to read stdin: {}", e); |
| 100 | process::exit(1); |
| 101 | } |
| 102 | let detected = lang.as_deref().unwrap_or("rust"); |
| 103 | if detected != "rust" { |
| 104 | eprintln!( |
| 105 | "warning: only Rust has full structural formatting; \ |
| 106 | '{}' will be lexed but not restructured", |
| 107 | detected, |
| 108 | ); |
| 109 | } |
| 110 | match format_source(&src, detected, &spec) { |
| 111 | Ok(formatted) => print!("{}", formatted), |
| 112 | Err(e) => { |
| 113 | eprintln!("error: {}", e); |
| 114 | process::exit(1); |
| 115 | } |
| 116 | } |
| 117 | return; |
| 118 | } |
| 119 | |
| 120 | if paths.is_empty() { |
| 121 | eprintln!("error: no input files"); |
| 122 | process::exit(2); |
| 123 | } |
| 124 | |
| 125 | // Expand directories recursively. |
| 126 | let files = match collect_files(&paths) { |
| 127 | Ok(f) => f, |
| 128 | Err(e) => { |
| 129 | eprintln!("error: {}", e); |
| 130 | process::exit(1); |
| 131 | } |
| 132 | }; |
| 133 | |
| 134 | if files.is_empty() { |
| 135 | eprintln!("error: no source files found"); |
| 136 | process::exit(2); |
| 137 | } |
| 138 | |
| 139 | let mut failures: usize = 0; |
| 140 | let mut would_change: Vec<String> = Vec::new(); |
| 141 | |
| 142 | for path in &files { |
| 143 | // An unrecognised extension is not Rust. Assuming it was meant |
| 144 | // a named .css or .html file is lexed as Rust and rewritten: |
| 145 | // `'Oxanium'` becomes `'Oxanium '` because the Rust lexer reads |
| 146 | // the opening quote as a lifetime. Say so and skip it instead. |
| 147 | let detected = match lang.as_deref() { |
| 148 | Some(l) => l, |
| 149 | None => match detect_language_from_ext(path) { |
| 150 | Some(l) => l, |
| 151 | None => { |
| 152 | eprintln!( |
| 153 | "error: {}: unrecognised extension; pass --lang to \ |
| 154 | say what it is", |
| 155 | path, |
| 156 | ); |
| 157 | failures += 1; |
| 158 | continue; |
| 159 | } |
| 160 | }, |
| 161 | }; |
| 162 | |
| 163 | let src = match std::fs::read_to_string(path) { |
| 164 | Ok(s) => s, |
| 165 | Err(e) => { |
| 166 | eprintln!("error: {}: {}", path, e); |
| 167 | failures += 1; |
| 168 | continue; |
| 169 | } |
| 170 | }; |
| 171 | |
| 172 | let formatted = match format_source(&src, detected, &spec) { |
| 173 | Ok(f) => f, |
| 174 | Err(e) => { |
| 175 | eprintln!("error: {}: {}", path, e); |
| 176 | failures += 1; |
| 177 | continue; |
| 178 | } |
| 179 | }; |
| 180 | |
| 181 | if check { |
| 182 | if formatted != src { |
| 183 | would_change.push(path.clone()); |
| 184 | } |
| 185 | } else if write { |
| 186 | if formatted != src { |
| 187 | if let Err(e) = std::fs::write(path, &formatted) { |
| 188 | eprintln!("error: {}: {}", path, e); |
| 189 | failures += 1; |
| 190 | } |
| 191 | } |
| 192 | } else { |
| 193 | print!("{}", formatted); |
| 194 | } |
| 195 | } |
| 196 | |
| 197 | if check { |
| 198 | if !would_change.is_empty() { |
| 199 | for p in &would_change { |
| 200 | println!("{}", p); |
| 201 | } |
| 202 | process::exit(1); |
| 203 | } |
| 204 | } |
| 205 | |
| 206 | if failures > 0 { |
| 207 | process::exit(1); |
| 208 | } |
| 209 | } |
| 210 | |
| 211 | fn format_source(src: &str, lang: &str, spec: &FormatSpec) -> Result<String, String> { |
| 212 | match lang { |
| 213 | "rust" => fmt::format_rust(src, spec).map_err(|e| format!("{}", e)), |
| 214 | _ => Err(format!( |
| 215 | "language '{}' is not yet supported for structural formatting \ |
| 216 | (supported: rust)", |
| 217 | lang, |
| 218 | )), |
| 219 | } |
| 220 | } |
| 221 | |
| 222 | fn detect_language_from_ext(path: &str) -> Option<&'static str> { |
| 223 | let ext = ok!(ok!(Path::new(path).extension()).to_str()); |
| 224 | match ext { |
| 225 | "rs" => Some("rust"), |
| 226 | "c" | "h" => Some("c"), |
| 227 | "cpp" | "cc" | "cxx" | "hpp" | "hh" | "hxx" => Some("cpp"), |
| 228 | "cs" => Some("csharp"), |
| 229 | "go" => Some("go"), |
| 230 | "java" => Some("java"), |
| 231 | "js" | "jsx" | "ts" | "tsx" | "mjs" | "cjs" => Some("javascript"), |
| 232 | "py" | "pyi" => Some("python"), |
| 233 | _ => None, |
| 234 | } |
| 235 | } |
| 236 | |
| 237 | /// A directory in `paths` is walked; a plain file is taken as given. |
| 238 | fn collect_files(paths: &[String]) -> Result<Vec<String>, String> { |
| 239 | let mut result = Vec::new(); |
| 240 | for path in paths { |
| 241 | let meta = ok!(std::fs::metadata(path) |
| 242 | .map_err(|e| format!("{}: {}", path, e))); |
| 243 | if meta.is_file() { |
| 244 | result.push(path.clone()); |
| 245 | } else if meta.is_dir() { |
| 246 | ok!(walk_dir(Path::new(path), &mut result)); |
| 247 | } |
| 248 | } |
| 249 | result.sort(); |
| 250 | Ok(result) |
| 251 | } |
| 252 | |
| 253 | /// Only files with a recognised extension are collected. |
| 254 | fn walk_dir(dir: &Path, out: &mut Vec<String>) -> Result<(), String> { |
| 255 | let entries = ok!(std::fs::read_dir(dir) |
| 256 | .map_err(|e| format!("{}: {}", dir.display(), e))); |
| 257 | for entry in entries { |
| 258 | let entry = ok!(entry.map_err(|e| format!("{}: {}", dir.display(), e))); |
| 259 | let path = entry.path(); |
| 260 | if path.is_dir() { |
| 261 | // Skip hidden directories and target/. |
| 262 | let name = path.file_name() |
| 263 | .and_then(|n| n.to_str()) |
| 264 | .unwrap_or(""); |
| 265 | if name.starts_with('.') || name == "target" { |
| 266 | continue; |
| 267 | } |
| 268 | ok!(walk_dir(&path, out)); |
| 269 | } else if path.is_file() { |
| 270 | let p = path.to_string_lossy().to_string(); |
| 271 | if detect_language_from_ext(&p).is_some() { |
| 272 | out.push(p); |
| 273 | } |
| 274 | } |
| 275 | } |
| 276 | Ok(()) |
| 277 | } |
| 278 | |
| 279 | fn print_usage() { |
| 280 | eprintln!("annealer -- the Oxedyne code formatter"); |
| 281 | eprintln!(); |
| 282 | eprintln!("USAGE:"); |
| 283 | eprintln!(" annealer [OPTIONS] [FILES/DIRS...]"); |
| 284 | eprintln!(); |
| 285 | eprintln!("OPTIONS:"); |
| 286 | eprintln!(" -w, --write Format files in place"); |
| 287 | eprintln!(" -c, --check Check formatting (exit 1 if changes needed)"); |
| 288 | eprintln!(" --stdin Read from stdin"); |
| 289 | eprintln!(" --lang <LANG> Override language detection ({})", |
| 290 | LANGS.join(", ")); |
| 291 | eprintln!(" --config <FILE> Load format specification from file"); |
| 292 | eprintln!(" -h, --help Show this help"); |
| 293 | eprintln!(); |
| 294 | eprintln!("Without --write or --check, formatted output goes to stdout."); |
| 295 | eprintln!("Directories are searched recursively for source files."); |
| 296 | } |