cairo_lang_formatter/
cairo_formatter.rs1use std::fmt::{Debug, Display};
2use std::fs;
3use std::io::{Read, stdin};
4use std::path::{Path, PathBuf};
5
6use anyhow::{Context, Result, anyhow};
7use cairo_lang_diagnostics::FormattedDiagnosticEntry;
8use cairo_lang_filesystem::db::FilesGroup;
9use cairo_lang_filesystem::ids::{CAIRO_FILE_EXTENSION, FileId, FileKind, FileLongId, VirtualFile};
10use cairo_lang_parser::utils::{SimpleParserDatabase, get_syntax_root_and_diagnostics};
11use cairo_lang_utils::Intern;
12use diffy::{PatchFormatter, create_patch};
13use ignore::WalkBuilder;
14use ignore::types::TypesBuilder;
15use thiserror::Error;
16
17use crate::{CAIRO_FMT_IGNORE, FormatterConfig, get_formatted_file};
18
19#[derive(Clone)]
24pub struct FileDiff {
25 pub original: String,
26 pub formatted: String,
27}
28
29impl FileDiff {
30 pub fn display_colored(&self) -> FileDiffColoredDisplay<'_> {
31 FileDiffColoredDisplay { diff: self }
32 }
33}
34
35impl Display for FileDiff {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 write!(f, "{}", create_patch(&self.original, &self.formatted))
38 }
39}
40
41impl Debug for FileDiff {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 writeln!(f, "FileDiff({self})")
44 }
45}
46
47pub struct FileDiffColoredDisplay<'a> {
52 diff: &'a FileDiff,
53}
54
55impl Display for FileDiffColoredDisplay<'_> {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 let patch = create_patch(&self.diff.original, &self.diff.formatted);
58 let patch_formatter = PatchFormatter::new().with_color();
59 let formatted_patch = patch_formatter.fmt_patch(&patch);
60 formatted_patch.fmt(f)
61 }
62}
63
64impl Debug for FileDiffColoredDisplay<'_> {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 writeln!(f, "FileDiffColoredDisplay({:?})", self.diff)
67 }
68}
69
70#[derive(Debug)]
75pub enum FormatOutcome {
76 Identical(String),
77 DiffFound(FileDiff),
78}
79
80impl FormatOutcome {
81 pub fn into_output_text(self) -> String {
82 match self {
83 FormatOutcome::Identical(original) => original,
84 FormatOutcome::DiffFound(diff) => diff.formatted,
85 }
86 }
87}
88
89#[derive(Debug, Error)]
91pub enum FormattingError {
92 #[error(transparent)]
94 ParsingError(ParsingError),
95 #[error(transparent)]
97 Error(#[from] anyhow::Error),
98}
99
100#[derive(Debug, Error)]
102pub struct ParsingError(Vec<FormattedDiagnosticEntry>);
103
104impl ParsingError {
105 pub fn iter(&self) -> impl Iterator<Item = &FormattedDiagnosticEntry> {
106 self.0.iter()
107 }
108}
109
110impl From<ParsingError> for Vec<FormattedDiagnosticEntry> {
111 fn from(error: ParsingError) -> Self {
112 error.0
113 }
114}
115
116impl From<Vec<FormattedDiagnosticEntry>> for ParsingError {
117 fn from(diagnostics: Vec<FormattedDiagnosticEntry>) -> Self {
118 Self(diagnostics)
119 }
120}
121
122impl Display for ParsingError {
123 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124 for entry in &self.0 {
125 writeln!(f, "{entry}")?;
126 }
127 Ok(())
128 }
129}
130
131pub struct StdinFmt;
134
135pub trait FormattableInput {
137 fn to_file_id(&self, db: &dyn FilesGroup) -> Result<FileId>;
139 fn overwrite_content(&self, _content: String) -> Result<()>;
141}
142
143impl FormattableInput for &Path {
144 fn to_file_id(&self, db: &dyn FilesGroup) -> Result<FileId> {
145 Ok(FileId::new(db, PathBuf::from(self)))
146 }
147 fn overwrite_content(&self, content: String) -> Result<()> {
148 fs::write(self, content)?;
149 Ok(())
150 }
151}
152
153impl FormattableInput for String {
154 fn to_file_id(&self, db: &dyn FilesGroup) -> Result<FileId> {
155 Ok(FileLongId::Virtual(VirtualFile {
156 parent: None,
157 name: "string_to_format".into(),
158 content: self.clone().into(),
159 code_mappings: [].into(),
160 kind: FileKind::Module,
161 original_item_removed: false,
162 })
163 .intern(db))
164 }
165
166 fn overwrite_content(&self, _content: String) -> Result<()> {
167 Ok(())
168 }
169}
170
171impl FormattableInput for StdinFmt {
172 fn to_file_id(&self, db: &dyn FilesGroup) -> Result<FileId> {
173 let mut buffer = String::new();
174 stdin().read_to_string(&mut buffer)?;
175 Ok(FileLongId::Virtual(VirtualFile {
176 parent: None,
177 name: "<stdin>".into(),
178 content: buffer.into(),
179 code_mappings: [].into(),
180 kind: FileKind::Module,
181 original_item_removed: false,
182 })
183 .intern(db))
184 }
185 fn overwrite_content(&self, content: String) -> Result<()> {
186 print!("{content}");
187 Ok(())
188 }
189}
190
191fn format_input(
192 input: &dyn FormattableInput,
193 config: &FormatterConfig,
194) -> Result<FormatOutcome, FormattingError> {
195 let db = SimpleParserDatabase::default();
196 let file_id = input.to_file_id(&db).context("Unable to create virtual file.")?;
197 let original_text =
198 db.file_content(file_id).ok_or_else(|| anyhow!("Unable to read from input."))?;
199 let (syntax_root, diagnostics) = get_syntax_root_and_diagnostics(&db, file_id, &original_text);
200 if diagnostics.check_error_free().is_err() {
201 return Err(FormattingError::ParsingError(
202 diagnostics.format_with_severity(&db, &Default::default()).into(),
203 ));
204 }
205 let formatted_text = get_formatted_file(&db, &syntax_root, config.clone());
206
207 if formatted_text == original_text.as_ref() {
208 Ok(FormatOutcome::Identical(original_text.to_string()))
209 } else {
210 let diff = FileDiff { original: original_text.to_string(), formatted: formatted_text };
211 Ok(FormatOutcome::DiffFound(diff))
212 }
213}
214
215#[derive(Debug)]
220pub struct CairoFormatter {
221 formatter_config: FormatterConfig,
222}
223
224impl CairoFormatter {
225 pub fn new(formatter_config: FormatterConfig) -> Self {
226 Self { formatter_config }
227 }
228
229 pub fn walk(&self, path: &Path) -> WalkBuilder {
233 let mut builder = WalkBuilder::new(path);
234 builder.add_custom_ignore_filename(CAIRO_FMT_IGNORE);
235 builder.follow_links(false);
236 builder.skip_stdout(true);
237
238 let mut types_builder = TypesBuilder::new();
239 types_builder.add(CAIRO_FILE_EXTENSION, &format!("*.{CAIRO_FILE_EXTENSION}")).unwrap();
240 types_builder.select(CAIRO_FILE_EXTENSION);
241 builder.types(types_builder.build().unwrap());
242
243 builder
244 }
245
246 pub fn format_in_place(
249 &self,
250 input: &dyn FormattableInput,
251 ) -> Result<FormatOutcome, FormattingError> {
252 match format_input(input, &self.formatter_config)? {
253 FormatOutcome::DiffFound(diff) => {
254 input.overwrite_content(diff.formatted.clone())?;
256 Ok(FormatOutcome::DiffFound(diff))
257 }
258 FormatOutcome::Identical(original) => Ok(FormatOutcome::Identical(original)),
259 }
260 }
261
262 pub fn format_to_string(
265 &self,
266 input: &dyn FormattableInput,
267 ) -> Result<FormatOutcome, FormattingError> {
268 format_input(input, &self.formatter_config)
269 }
270}