oxedyne/fe2o3/fe2o3_text/src/fmt/mod.rs
15.5 KiB, 9 runs
created by r1870400018:11630, 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 Hematite code formatter. |
| 2 | //! |
| 3 | //! A language-aware code formatter built on Wadler-style layout |
| 4 | //! algebra. The architecture separates three concerns: |
| 5 | //! |
| 6 | //! - **Lexing**: source text to token stream (per-language, data-driven). |
| 7 | //! - **Parsing**: token stream to concrete syntax tree (structural, keyword-aware). |
| 8 | //! - **Formatting**: CST to layout document to formatted text (universal algebra). |
| 9 | //! |
| 10 | //! The layout algebra (the `Doc` type) is a small set of combinators |
| 11 | //! that can express every formatting pattern across every language. |
| 12 | //! The renderer walks the document and makes optimal line-breaking |
| 13 | //! decisions within a given width. |
| 14 | //! |
| 15 | //! # Usage |
| 16 | //! |
| 17 | //! ```ignore |
| 18 | //! use oxedyne_fe2o3_text::fmt::{format_rust, format_source, spec::FormatSpec}; |
| 19 | //! |
| 20 | //! let source = "fn main ( ) { println!(\"hello\") ; }"; |
| 21 | //! let spec = FormatSpec::fe2o3(); |
| 22 | //! let formatted = res!(format_rust(source, &spec)); |
| 23 | //! |
| 24 | //! // Language-generic path. |
| 25 | //! let go_src = "func main() { fmt.Println(\"hello\") }"; |
| 26 | //! let formatted = res!(format_source(go_src, "go", &spec)); |
| 27 | //! ``` |
| 28 | //! |
| 29 | pub mod doc; |
| 30 | pub mod cst; |
| 31 | pub mod lex; |
| 32 | pub mod parse; |
| 33 | pub mod spec; |
| 34 | pub mod render; |
| 35 | pub mod format; |
| 36 | |
| 37 | use crate::fmt::spec::FormatSpec; |
| 38 | |
| 39 | use oxedyne_fe2o3_core::prelude::*; |
| 40 | |
| 41 | |
| 42 | /// Format Rust source code according to the given specification. |
| 43 | pub fn format_rust(source: &str, spec: &FormatSpec) -> Outcome<String> { |
| 44 | format::format_rust(source, spec) |
| 45 | } |
| 46 | |
| 47 | /// Format source code in the named language. |
| 48 | /// |
| 49 | /// Supported languages: `"rust"`, `"c"`, `"cpp"` (also `"c++"`), |
| 50 | /// `"csharp"` (also `"cs"`, `"c#"`), `"go"`, `"java"`, |
| 51 | /// `"js"` (also `"javascript"`, `"typescript"`, `"ts"`), |
| 52 | /// `"python"` (also `"py"`). |
| 53 | /// |
| 54 | /// Rust goes through the structural parser for keyword-aware |
| 55 | /// formatting. Other languages use the generic token-stream |
| 56 | /// pipeline, which handles indentation, spacing, and comment |
| 57 | /// preservation. |
| 58 | pub fn format_source(source: &str, lang: &str, spec: &FormatSpec) -> Outcome<String> { |
| 59 | match lang { |
| 60 | "rust" | "rs" => format_rust(source, spec), |
| 61 | _ => { |
| 62 | let lang_tokens = res!(lang_tokens_for(lang)); |
| 63 | format::format_with_lang(source, &lang_tokens, spec) |
| 64 | } |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | /// Detect language from a file path's extension. |
| 69 | pub fn detect_language(path: &str) -> Option<&'static str> { |
| 70 | let ext = path.rsplit('.').next(); |
| 71 | match ext { |
| 72 | Some("rs") => Some("rust"), |
| 73 | Some("c") => Some("c"), |
| 74 | Some("cpp") | Some("cxx") | Some("cc") |
| 75 | | Some("hpp") | Some("hxx") | Some("hh") => Some("cpp"), |
| 76 | Some("h") => Some("c"), |
| 77 | Some("cs") => Some("csharp"), |
| 78 | Some("go") => Some("go"), |
| 79 | Some("java") => Some("java"), |
| 80 | Some("js") | Some("jsx") | Some("mjs") |
| 81 | | Some("ts") | Some("tsx") => Some("js"), |
| 82 | Some("py") | Some("pyw") => Some("python"), |
| 83 | _ => None, |
| 84 | } |
| 85 | } |
| 86 | |
| 87 | /// Detect language from source text content. |
| 88 | /// |
| 89 | /// Examines the first 200 lines for shebang lines, keywords, and |
| 90 | /// syntactic patterns distinctive to each supported language. |
| 91 | /// Returns `None` if no language scores above the confidence |
| 92 | /// threshold. |
| 93 | pub fn detect_language_from_source(source: &str) -> Option<&'static str> { |
| 94 | // Shebang check. |
| 95 | if let Some(first) = source.lines().next() { |
| 96 | if first.starts_with("#!") { |
| 97 | let shebang = first.to_ascii_lowercase(); |
| 98 | if shebang.contains("python") || shebang.contains("python3") { |
| 99 | return Some("python"); |
| 100 | } |
| 101 | if shebang.contains("node") || shebang.contains("deno") || shebang.contains("bun") { |
| 102 | return Some("js"); |
| 103 | } |
| 104 | if shebang.contains("java") { |
| 105 | return Some("java"); |
| 106 | } |
| 107 | } |
| 108 | } |
| 109 | |
| 110 | let mut rust: i32 = 0; |
| 111 | let mut c: i32 = 0; |
| 112 | let mut cpp: i32 = 0; |
| 113 | let mut csharp: i32 = 0; |
| 114 | let mut go: i32 = 0; |
| 115 | let mut java: i32 = 0; |
| 116 | let mut js: i32 = 0; |
| 117 | let mut python: i32 = 0; |
| 118 | |
| 119 | let lines: Vec<&str> = source.lines().take(200).collect(); |
| 120 | |
| 121 | for line in &lines { |
| 122 | let trimmed = line.trim(); |
| 123 | |
| 124 | // Rust. |
| 125 | if trimmed.starts_with("pub fn ") |
| 126 | || trimmed.starts_with("pub struct ") |
| 127 | || trimmed.starts_with("pub enum ") |
| 128 | || trimmed.starts_with("pub trait ") { rust += 5; } |
| 129 | if trimmed.starts_with("fn ") { rust += 3; } |
| 130 | if trimmed.starts_with("let mut ") { rust += 4; } |
| 131 | if trimmed.starts_with("mod ") { rust += 2; } |
| 132 | if trimmed.starts_with("impl ") { rust += 5; } |
| 133 | if trimmed.starts_with("use ") && trimmed.contains("::"){ rust += 4; } |
| 134 | if trimmed.starts_with("#[") || trimmed.starts_with("#![") { rust += 4; } |
| 135 | if trimmed.starts_with("///") || trimmed.starts_with("//!") { rust += 3; } |
| 136 | if trimmed.contains("-> ") && !trimmed.starts_with("//") { rust += 2; } |
| 137 | if trimmed.contains("&self") { rust += 5; } |
| 138 | if trimmed.contains("&str") { rust += 4; } |
| 139 | if trimmed.contains("macro_rules!") { rust += 6; } |
| 140 | if trimmed.contains("res!(") || trimmed.contains("ok!(") { rust += 3; } |
| 141 | if trimmed.starts_with("match ") { rust += 2; } |
| 142 | |
| 143 | // C. |
| 144 | if trimmed.starts_with("#include") { c += 6; } |
| 145 | if trimmed.starts_with("#define") { c += 4; } |
| 146 | if trimmed.starts_with("typedef ") { c += 5; } |
| 147 | if trimmed.contains("sizeof(") { c += 3; } |
| 148 | if trimmed.contains("malloc(") || trimmed.contains("free(") { c += 4; } |
| 149 | if trimmed.contains("printf(") || trimmed.contains("fprintf(") { c += 3; } |
| 150 | if trimmed.contains("NULL") { c += 2; } |
| 151 | if trimmed.starts_with("void ") { c += 2; } |
| 152 | if trimmed.contains("int main") { c += 3; } |
| 153 | |
| 154 | // C++. |
| 155 | if trimmed.starts_with("#include <") && ( |
| 156 | trimmed.contains("<iostream>") || trimmed.contains("<vector>") |
| 157 | || trimmed.contains("<string>") || trimmed.contains("<map>") |
| 158 | || trimmed.contains("<memory>") || trimmed.contains("<algorithm>") |
| 159 | || trimmed.contains("<functional>") || trimmed.contains("<array>") |
| 160 | || trimmed.contains("<optional>") || trimmed.contains("<variant>")) |
| 161 | { cpp += 6; } |
| 162 | if trimmed.starts_with("namespace ") { cpp += 5; } |
| 163 | if trimmed.starts_with("class ") && trimmed.contains('{') { cpp += 2; } |
| 164 | if trimmed.starts_with("template") { cpp += 6; } |
| 165 | if trimmed.contains("std::") { cpp += 5; } |
| 166 | if trimmed.contains("cout") || trimmed.contains("cerr") { cpp += 4; } |
| 167 | if trimmed.contains("nullptr") { cpp += 5; } |
| 168 | if trimmed.contains("auto ") && trimmed.contains("= ") |
| 169 | && !trimmed.starts_with("#") { cpp += 3; } |
| 170 | if trimmed.contains("const auto") { cpp += 4; } |
| 171 | if trimmed.contains("unique_ptr") || trimmed.contains("shared_ptr") |
| 172 | || trimmed.contains("make_unique") || trimmed.contains("make_shared") |
| 173 | { cpp += 6; } |
| 174 | if trimmed.contains("static_cast") || trimmed.contains("dynamic_cast") |
| 175 | || trimmed.contains("reinterpret_cast") { cpp += 6; } |
| 176 | if trimmed.starts_with("virtual ") { cpp += 4; } |
| 177 | if trimmed.contains("constexpr ") { cpp += 5; } |
| 178 | if trimmed.contains("noexcept") { cpp += 4; } |
| 179 | if trimmed.starts_with("using namespace") { cpp += 5; } |
| 180 | if trimmed.starts_with("#include") && !trimmed.contains('<') { cpp += 2; } |
| 181 | if trimmed.contains("override") && trimmed.contains('{') { cpp += 3; } |
| 182 | |
| 183 | // C#. |
| 184 | if trimmed.starts_with("using System") { csharp += 6; } |
| 185 | if trimmed.starts_with("namespace ") |
| 186 | && !trimmed.contains("::") { csharp += 4; } |
| 187 | if trimmed.contains("Console.Write") { csharp += 5; } |
| 188 | if trimmed.starts_with("[") && ( |
| 189 | trimmed.contains("Serializable") || trimmed.contains("Attribute") |
| 190 | || trimmed.contains("Test") || trimmed.contains("HttpGet") |
| 191 | || trimmed.contains("HttpPost") || trimmed.contains("Required") |
| 192 | || trimmed.contains("JsonProperty") || trimmed.contains("Obsolete")) |
| 193 | { csharp += 4; } |
| 194 | if trimmed.contains("string.") || trimmed.contains("String.") |
| 195 | && !trimmed.contains("import java.") { csharp += 2; } |
| 196 | if trimmed.starts_with("public class ") && !trimmed.contains("static void main") |
| 197 | { csharp += 2; } |
| 198 | if trimmed.contains("async Task") || trimmed.contains("Task<") { csharp += 5; } |
| 199 | if trimmed.contains("IEnumerable") || trimmed.contains("IList") |
| 200 | || trimmed.contains("IDictionary") { csharp += 5; } |
| 201 | if trimmed.starts_with("foreach ") { csharp += 5; } |
| 202 | if trimmed.contains("var ") && trimmed.contains("= new ") { csharp += 3; } |
| 203 | if trimmed.contains("get;") || trimmed.contains("set;") { csharp += 5; } |
| 204 | if trimmed.contains("=> ") && trimmed.contains(';') |
| 205 | && !trimmed.contains("===") { csharp += 2; } |
| 206 | if trimmed.starts_with("internal ") { csharp += 5; } |
| 207 | if trimmed.starts_with("sealed ") { csharp += 5; } |
| 208 | if trimmed.starts_with("partial class") |
| 209 | || trimmed.starts_with("partial struct") { csharp += 6; } |
| 210 | if trimmed.contains("nameof(") { csharp += 5; } |
| 211 | if trimmed.contains(".Select(") || trimmed.contains(".Where(") |
| 212 | || trimmed.contains(".OrderBy(") { csharp += 3; } |
| 213 | if trimmed.starts_with("record ") { csharp += 5; } |
| 214 | |
| 215 | // Go. |
| 216 | if trimmed.starts_with("package ") { go += 6; } |
| 217 | if trimmed.starts_with("func ") { go += 5; } |
| 218 | if trimmed.contains(":= ") { go += 4; } |
| 219 | if trimmed.starts_with("import (") { go += 5; } |
| 220 | if trimmed.starts_with("defer ") { go += 5; } |
| 221 | if trimmed.contains("go func") { go += 5; } |
| 222 | if trimmed.contains("chan ") { go += 4; } |
| 223 | if trimmed.contains("fmt.") { go += 3; } |
| 224 | if trimmed.starts_with("type ") && trimmed.contains("struct {") { go += 5; } |
| 225 | if trimmed.starts_with("var ") { go += 1; } |
| 226 | |
| 227 | // Java. |
| 228 | if trimmed.starts_with("public class ") { java += 6; } |
| 229 | if trimmed.starts_with("private ") |
| 230 | || trimmed.starts_with("protected ") { java += 3; } |
| 231 | if trimmed.contains("System.out") { java += 5; } |
| 232 | if trimmed.starts_with("import java.") |
| 233 | || trimmed.starts_with("import javax.") { java += 6; } |
| 234 | if trimmed.starts_with("@Override") |
| 235 | || trimmed.starts_with("@Deprecated") { java += 4; } |
| 236 | if trimmed.contains("public static void main") { java += 6; } |
| 237 | if trimmed.contains("throws ") { java += 3; } |
| 238 | if trimmed.starts_with("package ") && trimmed.contains(';') { java += 4; } |
| 239 | |
| 240 | // JavaScript / TypeScript. |
| 241 | if trimmed.starts_with("const ") && trimmed.contains("= ") { js += 2; } |
| 242 | if trimmed.starts_with("function ") { js += 3; } |
| 243 | if trimmed.contains("=> {") || trimmed.contains("=> (") { js += 4; } |
| 244 | if trimmed.contains("===") || trimmed.contains("!==") { js += 5; } |
| 245 | if trimmed.contains("require(") { js += 5; } |
| 246 | if trimmed.contains("console.") { js += 4; } |
| 247 | if trimmed.starts_with("export ") { js += 3; } |
| 248 | if trimmed.starts_with("import ") && trimmed.contains("from ") { js += 5; } |
| 249 | if trimmed.starts_with("interface ") { js += 3; } |
| 250 | if trimmed.contains("document.") || trimmed.contains("window.") { js += 4; } |
| 251 | if trimmed.starts_with("async function") { js += 4; } |
| 252 | |
| 253 | // Python. |
| 254 | if trimmed.starts_with("def ") { python += 4; } |
| 255 | if trimmed.starts_with("class ") && trimmed.ends_with(':') { python += 5; } |
| 256 | if trimmed.contains("self.") { python += 4; } |
| 257 | if trimmed.starts_with("elif ") { python += 6; } |
| 258 | if trimmed.starts_with("from ") && trimmed.contains("import ") { python += 5; } |
| 259 | if trimmed.starts_with("import ") && !trimmed.contains('{') |
| 260 | && !trimmed.contains("java.") && !trimmed.contains("from ") { python += 2; } |
| 261 | if trimmed.starts_with("# ") && !trimmed.starts_with("#include") |
| 262 | && !trimmed.starts_with("#define") && !trimmed.starts_with("#[") { python += 1; } |
| 263 | if trimmed.contains("__init__") || trimmed.contains("__main__") { python += 6; } |
| 264 | if trimmed.contains("None") && !trimmed.starts_with("//") { python += 1; } |
| 265 | if trimmed.starts_with("@") && !trimmed.starts_with("@Override") |
| 266 | && !trimmed.starts_with("@Deprecated") { python += 1; } |
| 267 | if (trimmed.starts_with("if ") || trimmed.starts_with("for ") |
| 268 | || trimmed.starts_with("while ")) && trimmed.ends_with(':') { python += 2; } |
| 269 | } |
| 270 | |
| 271 | let scores = [ |
| 272 | (rust, "rust"), |
| 273 | (c, "c"), |
| 274 | (cpp, "cpp"), |
| 275 | (csharp, "csharp"), |
| 276 | (go, "go"), |
| 277 | (java, "java"), |
| 278 | (js, "js"), |
| 279 | (python, "python"), |
| 280 | ]; |
| 281 | |
| 282 | let (best_score, best_lang) = scores.iter() |
| 283 | .fold((0, ""), |(bs, bl), &(s, l)| if s > bs { (s, l) } else { (bs, bl) }); |
| 284 | |
| 285 | // Require a minimum confidence. |
| 286 | if best_score >= 5 { |
| 287 | Some(best_lang) |
| 288 | } else { |
| 289 | None |
| 290 | } |
| 291 | } |
| 292 | |
| 293 | /// Look up the `LangTokens` for a language name. |
| 294 | fn lang_tokens_for(lang: &str) -> Outcome<lex::LangTokens> { |
| 295 | match lang { |
| 296 | "c" => Ok(lex::c_tokens()), |
| 297 | "cpp" | "c++" => Ok(lex::cpp_tokens()), |
| 298 | "csharp" | "cs" | "c#" => Ok(lex::csharp_tokens()), |
| 299 | "go" => Ok(lex::go_tokens()), |
| 300 | "java" => Ok(lex::java_tokens()), |
| 301 | "js" | "javascript" | "typescript" | "ts" => Ok(lex::js_tokens()), |
| 302 | "python" | "py" => Ok(lex::python_tokens()), |
| 303 | _ => Err(err!( |
| 304 | "Unsupported language: '{}'. Supported: rust, c, cpp, csharp, go, java, js, python", |
| 305 | lang; |
| 306 | Invalid, Input |
| 307 | )), |
| 308 | } |
| 309 | } |