cairo_lang_formatter/
cairo_formatter.rs

1use 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/// A struct encapsulating the changes made by the formatter in a single file.
20///
21/// This struct implements [`Display`] and [`Debug`] traits, showing differences between
22/// the original and modified file content.
23#[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
47/// A helper struct for displaying a file diff with colored output.
48///
49/// This is implements a [`Display`] trait, so it can be used with `format!` and `println!`.
50/// If you prefer output without colors, use [`FileDiff`] instead.
51pub 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/// An output from file formatting.
71///
72/// Differentiates between already formatted files and files that differ after formatting.
73/// Contains the original file content and the formatted file content.
74#[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/// An error thrown while trying to format Cairo code.
90#[derive(Debug, Error)]
91pub enum FormattingError {
92    /// A parsing error has occurred. See diagnostics for context.
93    #[error(transparent)]
94    ParsingError(ParsingError),
95    /// All other errors.
96    #[error(transparent)]
97    Error(#[from] anyhow::Error),
98}
99
100/// Parsing error representation with diagnostic entries.
101#[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
131/// A struct used to indicate that the formatter input should be read from stdin.
132/// Implements the [`FormattableInput`] trait.
133pub struct StdinFmt;
134
135/// A trait for types that can be used as input for the cairo formatter.
136pub trait FormattableInput {
137    /// Converts the input to a [`FileId`] that can be used by the formatter.
138    fn to_file_id(&self, db: &dyn FilesGroup) -> Result<FileId>;
139    /// Overwrites the content of the input with the given string.
140    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/// A struct for formatting cairo files.
216///
217/// The formatter can operate on all types implementing the [`FormattableInput`] trait.
218/// Allows formatting in place, and for formatting to a string.
219#[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    /// Returns a preconfigured `ignore::WalkBuilder` for the given path.
230    ///
231    /// Can be used for recursively formatting a directory under given path.
232    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    /// Formats the path in place, writing changes to the files.
247    /// The ['FormattableInput'] trait implementation defines the method for persisting changes.
248    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                // Persist changes.
255                input.overwrite_content(diff.formatted.clone())?;
256                Ok(FormatOutcome::DiffFound(diff))
257            }
258            FormatOutcome::Identical(original) => Ok(FormatOutcome::Identical(original)),
259        }
260    }
261
262    /// Formats the path and returns the formatted string.
263    /// No changes are persisted. The original file is not modified.
264    pub fn format_to_string(
265        &self,
266        input: &dyn FormattableInput,
267    ) -> Result<FormatOutcome, FormattingError> {
268        format_input(input, &self.formatter_config)
269    }
270}