Oregami
Repositories/oxedyne/fe2o3

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
10use oxedyne_fe2o3_core::ok;
11use oxedyne_fe2o3_text::fmt::{
12 self,
13 spec::FormatSpec,
14};
15
16use std::io::Read;
17use std::path::Path;
18use std::process;
19
20
21const LANGS: &[&str] = &["rust", "c", "cpp", "csharp", "go", "java", "javascript", "python"];
22
23fn 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
211fn 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
222fn 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.
238fn 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.
254fn 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
279fn 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}