1#![doc = include_str!("../README.md")]
2#![cfg_attr(not(test), warn(unused_crate_dependencies))]
3#![cfg_attr(docsrs, feature(doc_cfg))]
4
5const DEBUG: bool = false || option_env!("FMT_DEBUG").is_some();
6const DEBUG_INDENT: bool = false || option_env!("FMT_DEBUG").is_some();
7
8use foundry_common::comments::{
9 Comment, Comments,
10 inline_config::{InlineConfig, InlineConfigItem},
11};
12
13mod state;
14
15mod pp;
16
17use solar::{
18 parse::{
19 ast::{SourceUnit, Span},
20 interface::{Session, diagnostics::EmittedDiagnostics, source_map::SourceFile},
21 },
22 sema::{Compiler, Gcx, Source},
23};
24
25use std::{path::Path, sync::Arc};
26
27pub use foundry_config::fmt::*;
28
29pub type FormatterResult = DiagnosticsResult<String, EmittedDiagnostics>;
31
32#[derive(Debug)]
34pub enum DiagnosticsResult<T, E> {
35 Ok(T),
37 OkWithDiagnostics(T, E),
39 ErrRecovered(T, E),
41 Err(E),
43}
44
45impl<T, E> DiagnosticsResult<T, E> {
46 pub fn into_result(self) -> Result<T, E> {
50 match self {
51 Self::Ok(s) | Self::OkWithDiagnostics(s, _) => Ok(s),
52 Self::ErrRecovered(_, d) | Self::Err(d) => Err(d),
53 }
54 }
55
56 pub fn into_ok(self) -> Result<T, E> {
58 match self {
59 Self::Ok(s) | Self::OkWithDiagnostics(s, _) | Self::ErrRecovered(s, _) => Ok(s),
60 Self::Err(e) => Err(e),
61 }
62 }
63
64 pub fn ok_ref(&self) -> Option<&T> {
66 match self {
67 Self::Ok(s) | Self::OkWithDiagnostics(s, _) | Self::ErrRecovered(s, _) => Some(s),
68 Self::Err(_) => None,
69 }
70 }
71
72 pub fn err_ref(&self) -> Option<&E> {
74 match self {
75 Self::Ok(_) => None,
76 Self::OkWithDiagnostics(_, d) | Self::ErrRecovered(_, d) | Self::Err(d) => Some(d),
77 }
78 }
79
80 pub fn is_ok(&self) -> bool {
82 matches!(self, Self::Ok(_) | Self::OkWithDiagnostics(_, _))
83 }
84
85 pub fn is_err(&self) -> bool {
87 !self.is_ok()
88 }
89}
90
91pub fn format_file(
92 path: &Path,
93 config: Arc<FormatterConfig>,
94 compiler: &mut Compiler,
95) -> FormatterResult {
96 format_inner(config, compiler, &|sess| {
97 sess.source_map().load_file(path).map_err(|e| sess.dcx.err(e.to_string()).emit())
98 })
99}
100
101pub fn format_source(
102 source: &str,
103 path: Option<&Path>,
104 config: Arc<FormatterConfig>,
105 compiler: &mut Compiler,
106) -> FormatterResult {
107 format_inner(config, compiler, &|sess| {
108 let name = match path {
109 Some(path) => solar::parse::interface::source_map::FileName::Real(path.to_path_buf()),
110 None => solar::parse::interface::source_map::FileName::Stdin,
111 };
112 sess.source_map()
113 .new_source_file(name, source)
114 .map_err(|e| sess.dcx.err(e.to_string()).emit())
115 })
116}
117
118pub fn format(source: &str, config: FormatterConfig) -> FormatterResult {
120 let mut compiler = Compiler::new(
121 solar::interface::Session::builder().with_buffer_emitter(Default::default()).build(),
122 );
123
124 format_source(source, None, Arc::new(config), &mut compiler)
125}
126
127fn format_inner(
128 config: Arc<FormatterConfig>,
129 compiler: &mut Compiler,
130 mk_file: &(dyn Fn(&Session) -> solar::parse::interface::Result<Arc<SourceFile>> + Sync + Send),
131) -> FormatterResult {
132 let first_result = format_once(config.clone(), compiler, mk_file);
134
135 if first_result.is_err() {
137 return first_result;
138 }
139 let Some(first_formatted) = first_result.ok_ref() else { return first_result };
140
141 let second_result = format_once(config, compiler, &|sess| {
143 let prev_sf = mk_file(sess)?;
145 let new_name = match &prev_sf.name {
146 solar::interface::source_map::FileName::Real(path) => {
147 path.with_extension("again.sol").into()
148 }
149 solar::interface::source_map::FileName::Stdin => {
150 solar::interface::source_map::FileName::Custom("stdin-again".to_string())
151 }
152 solar::interface::source_map::FileName::Custom(name) => {
153 solar::interface::source_map::FileName::Custom(format!("{name}-again"))
154 }
155 };
156 sess.source_map()
157 .new_source_file(new_name, first_formatted)
158 .map_err(|e| sess.dcx.err(e.to_string()).emit())
159 });
160
161 match (first_result.ok_ref(), second_result.ok_ref()) {
163 (Some(first), Some(second)) if first != second => {
164 panic!("formatter is not idempotent:\n{}", diff(first, second));
165 }
166 _ => {}
167 }
168
169 if first_result.is_ok() && second_result.is_err() && !DEBUG {
170 panic!(
171 "failed to format a second time:\nfirst_result={first_result:#?}\nsecond_result={second_result:#?}"
172 );
173 } else {
175 first_result
176 }
177}
178
179fn diff(first: &str, second: &str) -> impl std::fmt::Display {
180 use std::fmt::Write;
181 let diff = similar::TextDiff::from_lines(first, second);
182 let mut s = String::new();
183 for change in diff.iter_all_changes() {
184 let tag = match change.tag() {
185 similar::ChangeTag::Delete => "-",
186 similar::ChangeTag::Insert => "+",
187 similar::ChangeTag::Equal => " ",
188 };
189 write!(s, "{tag}{change}").unwrap();
190 }
191 s
192}
193
194fn format_once(
195 config: Arc<FormatterConfig>,
196 compiler: &mut Compiler,
197 mk_file: &(
198 dyn Fn(&solar::interface::Session) -> solar::interface::Result<Arc<SourceFile>>
199 + Send
200 + Sync
201 ),
202) -> FormatterResult {
203 let res = compiler.enter_mut(|c| -> solar::interface::Result<String> {
204 let mut pcx = c.parse();
205 pcx.set_resolve_imports(false);
206 let file = mk_file(c.sess())?;
207 pcx.add_file(file.clone());
208 pcx.parse();
209 c.dcx().has_errors()?;
210
211 let gcx = c.gcx();
212 let (_, source) = gcx.get_ast_source(&file.name).unwrap();
213 Ok(format_ast(gcx, source, config).expect("unable to format AST"))
214 });
215
216 let diagnostics = compiler.sess().dcx.emitted_diagnostics().unwrap();
217 match (res, compiler.sess().dcx.has_errors()) {
218 (Ok(s), Ok(())) if diagnostics.is_empty() => FormatterResult::Ok(s),
219 (Ok(s), Ok(())) => FormatterResult::OkWithDiagnostics(s, diagnostics),
220 (Ok(s), Err(_)) => FormatterResult::ErrRecovered(s, diagnostics),
221 (Err(_), Ok(_)) => unreachable!(),
222 (Err(_), Err(_)) => FormatterResult::Err(diagnostics),
223 }
224}
225
226pub fn format_ast<'ast>(
228 gcx: Gcx<'ast>,
229 source: &'ast Source<'ast>,
230 config: Arc<FormatterConfig>,
231) -> Option<String> {
232 let comments = Comments::new(
233 &source.file,
234 gcx.sess.source_map(),
235 true,
236 config.wrap_comments,
237 if matches!(config.style, IndentStyle::Tab) { Some(config.tab_width) } else { None },
238 );
239 let ast = source.ast.as_ref()?;
240 let inline_config = parse_inline_config(gcx.sess, &comments, ast);
241
242 let mut state = state::State::new(gcx.sess.source_map(), config, inline_config, comments);
243 state.print_source_unit(ast);
244 Some(state.s.eof())
245}
246
247fn parse_inline_config<'ast>(
248 sess: &Session,
249 comments: &Comments,
250 ast: &'ast SourceUnit<'ast>,
251) -> InlineConfig<()> {
252 let parse_item = |mut item: &str, cmnt: &Comment| -> Option<(Span, InlineConfigItem<()>)> {
253 if let Some(prefix) = cmnt.prefix() {
254 item = item.strip_prefix(prefix).unwrap_or(item);
255 }
256 if let Some(suffix) = cmnt.suffix() {
257 item = item.strip_suffix(suffix).unwrap_or(item);
258 }
259 let item = item.trim_start().strip_prefix("forgefmt:")?.trim();
260 match item.parse::<InlineConfigItem<()>>() {
261 Ok(item) => Some((cmnt.span, item)),
262 Err(e) => {
263 sess.dcx.warn(e.to_string()).span(cmnt.span).emit();
264 None
265 }
266 }
267 };
268
269 let items = comments.iter().flat_map(|cmnt| {
270 let mut found_items = Vec::with_capacity(2);
271 if let Some(line) = cmnt.lines.first()
273 && let Some(item) = parse_item(line, cmnt)
274 {
275 found_items.push(item);
276 }
277 if cmnt.lines.len() > 1
279 && let Some(line) = cmnt.lines.last()
280 && let Some(item) = parse_item(line, cmnt)
281 {
282 found_items.push(item);
283 }
284 found_items
285 });
286
287 InlineConfig::from_ast(items, ast, sess.source_map())
288}