forge_doc/writer/
as_doc.rs

1use crate::{
2    document::{read_context, DocumentContent},
3    parser::ParseSource,
4    writer::BufWriter,
5    CommentTag, Comments, CommentsRef, Document, Markdown, PreprocessorOutput,
6    CONTRACT_INHERITANCE_ID, DEPLOYMENTS_ID, GIT_SOURCE_ID, INHERITDOC_ID,
7};
8use forge_fmt::solang_ext::SafeUnwrap;
9use itertools::Itertools;
10use solang_parser::pt::{Base, FunctionDefinition};
11use std::path::{Path, PathBuf};
12
13/// The result of [`AsDoc::as_doc`].
14pub type AsDocResult = Result<String, std::fmt::Error>;
15
16/// A trait for formatting a parse unit as documentation.
17pub trait AsDoc {
18    /// Formats a parse tree item into a doc string.
19    fn as_doc(&self) -> AsDocResult;
20}
21
22impl AsDoc for String {
23    fn as_doc(&self) -> AsDocResult {
24        Ok(self.to_owned())
25    }
26}
27
28impl AsDoc for Comments {
29    fn as_doc(&self) -> AsDocResult {
30        CommentsRef::from(self).as_doc()
31    }
32}
33
34impl AsDoc for CommentsRef<'_> {
35    // TODO: support other tags
36    fn as_doc(&self) -> AsDocResult {
37        let mut writer = BufWriter::default();
38
39        // Write author tag(s)
40        let authors = self.include_tag(CommentTag::Author);
41        if !authors.is_empty() {
42            writer.write_bold(&format!("Author{}:", if authors.len() == 1 { "" } else { "s" }))?;
43            writer.writeln_raw(authors.iter().map(|a| &a.value).join(", "))?;
44            writer.writeln()?;
45        }
46
47        // Write notice tags
48        let notices = self.include_tag(CommentTag::Notice);
49        for n in notices.iter() {
50            writer.writeln_raw(&n.value)?;
51            writer.writeln()?;
52        }
53
54        // Write dev tags
55        let devs = self.include_tag(CommentTag::Dev);
56        for d in devs.iter() {
57            writer.write_italic(&d.value)?;
58            writer.writeln()?;
59        }
60
61        // Write custom tags
62        let customs = self.get_custom_tags();
63        if !customs.is_empty() {
64            writer.write_bold(&format!("Note{}:", if customs.len() == 1 { "" } else { "s" }))?;
65            for c in customs.iter() {
66                writer.writeln_raw(format!(
67                    "{}{}: {}",
68                    if customs.len() == 1 { "" } else { "- " },
69                    &c.tag,
70                    &c.value
71                ))?;
72                writer.writeln()?;
73            }
74        }
75
76        Ok(writer.finish())
77    }
78}
79
80impl AsDoc for Base {
81    fn as_doc(&self) -> AsDocResult {
82        Ok(self.name.identifiers.iter().map(|ident| ident.name.to_owned()).join("."))
83    }
84}
85
86impl AsDoc for Document {
87    fn as_doc(&self) -> AsDocResult {
88        let mut writer = BufWriter::default();
89
90        match &self.content {
91            DocumentContent::OverloadedFunctions(items) => {
92                writer
93                    .write_title(&format!("function {}", items.first().unwrap().source.ident()))?;
94                if let Some(git_source) = read_context!(self, GIT_SOURCE_ID, GitSource) {
95                    writer.write_link("Git Source", &git_source)?;
96                    writer.writeln()?;
97                }
98
99                for item in items {
100                    let func = item.as_function().unwrap();
101                    let mut heading = item.source.ident();
102                    if !func.params.is_empty() {
103                        heading.push_str(&format!(
104                            "({})",
105                            func.params
106                                .iter()
107                                .map(|p| p.1.as_ref().map(|p| p.ty.to_string()).unwrap_or_default())
108                                .join(", ")
109                        ));
110                    }
111                    writer.write_heading(&heading)?;
112                    writer.write_section(&item.comments, &item.code)?;
113                }
114            }
115            DocumentContent::Constants(items) => {
116                writer.write_title("Constants")?;
117                if let Some(git_source) = read_context!(self, GIT_SOURCE_ID, GitSource) {
118                    writer.write_link("Git Source", &git_source)?;
119                    writer.writeln()?;
120                }
121
122                for item in items {
123                    let var = item.as_variable().unwrap();
124                    writer.write_heading(&var.name.safe_unwrap().name)?;
125                    writer.write_section(&item.comments, &item.code)?;
126                }
127            }
128            DocumentContent::Single(item) => {
129                writer.write_title(&item.source.ident())?;
130                if let Some(git_source) = read_context!(self, GIT_SOURCE_ID, GitSource) {
131                    writer.write_link("Git Source", &git_source)?;
132                    writer.writeln()?;
133                }
134
135                if let Some(deployments) = read_context!(self, DEPLOYMENTS_ID, Deployments) {
136                    writer.write_deployments_table(deployments)?;
137                }
138
139                match &item.source {
140                    ParseSource::Contract(contract) => {
141                        if !contract.base.is_empty() {
142                            writer.write_bold("Inherits:")?;
143
144                            // we need this to find the _relative_ paths
145                            let src_target_dir = self.target_src_dir();
146
147                            let mut bases = vec![];
148                            let linked =
149                                read_context!(self, CONTRACT_INHERITANCE_ID, ContractInheritance);
150                            for base in &contract.base {
151                                let base_doc = base.as_doc()?;
152                                let base_ident = &base.name.identifiers.last().unwrap().name;
153
154                                let link = linked
155                                    .as_ref()
156                                    .and_then(|link| {
157                                        link.get(base_ident).map(|path| {
158                                            let path = Path::new("/").join(
159                                                path.strip_prefix(&src_target_dir)
160                                                    .ok()
161                                                    .unwrap_or(path),
162                                            );
163                                            Markdown::Link(&base_doc, &path.display().to_string())
164                                                .as_doc()
165                                        })
166                                    })
167                                    .transpose()?
168                                    .unwrap_or(base_doc);
169
170                                bases.push(link);
171                            }
172
173                            writer.writeln_raw(bases.join(", "))?;
174                            writer.writeln()?;
175                        }
176
177                        writer.writeln_doc(&item.comments)?;
178
179                        if let Some(state_vars) = item.variables() {
180                            writer.write_subtitle("State Variables")?;
181                            state_vars.into_iter().try_for_each(|(item, comments, code)| {
182                                let comments = comments.merge_inheritdoc(
183                                    &item.name.safe_unwrap().name,
184                                    read_context!(self, INHERITDOC_ID, Inheritdoc),
185                                );
186
187                                writer.write_heading(&item.name.safe_unwrap().name)?;
188                                writer.write_section(&comments, code)?;
189                                writer.writeln()
190                            })?;
191                        }
192
193                        if let Some(funcs) = item.functions() {
194                            writer.write_subtitle("Functions")?;
195
196                            for (func, comments, code) in &funcs {
197                                self.write_function(&mut writer, func, comments, code)?;
198                            }
199                        }
200
201                        if let Some(events) = item.events() {
202                            writer.write_subtitle("Events")?;
203                            events.into_iter().try_for_each(|(item, comments, code)| {
204                                writer.write_heading(&item.name.safe_unwrap().name)?;
205                                writer.write_section(comments, code)?;
206                                writer.try_write_events_table(&item.fields, comments)
207                            })?;
208                        }
209
210                        if let Some(errors) = item.errors() {
211                            writer.write_subtitle("Errors")?;
212                            errors.into_iter().try_for_each(|(item, comments, code)| {
213                                writer.write_heading(&item.name.safe_unwrap().name)?;
214                                writer.write_section(comments, code)?;
215                                writer.try_write_errors_table(&item.fields, comments)
216                            })?;
217                        }
218
219                        if let Some(structs) = item.structs() {
220                            writer.write_subtitle("Structs")?;
221                            structs.into_iter().try_for_each(|(item, comments, code)| {
222                                writer.write_heading(&item.name.safe_unwrap().name)?;
223                                writer.write_section(comments, code)?;
224                                writer.try_write_properties_table(&item.fields, comments)
225                            })?;
226                        }
227
228                        if let Some(enums) = item.enums() {
229                            writer.write_subtitle("Enums")?;
230                            enums.into_iter().try_for_each(|(item, comments, code)| {
231                                writer.write_heading(&item.name.safe_unwrap().name)?;
232                                writer.write_section(comments, code)
233                            })?;
234                        }
235                    }
236
237                    ParseSource::Function(func) => {
238                        // TODO: cleanup
239                        // Write function docs
240                        writer.writeln_doc(
241                            &item.comments.exclude_tags(&[CommentTag::Param, CommentTag::Return]),
242                        )?;
243
244                        // Write function header
245                        writer.write_code(&item.code)?;
246
247                        // Write function parameter comments in a table
248                        let params =
249                            func.params.iter().filter_map(|p| p.1.as_ref()).collect::<Vec<_>>();
250                        writer.try_write_param_table(CommentTag::Param, &params, &item.comments)?;
251
252                        // Write function return parameter comments in a table
253                        let returns =
254                            func.returns.iter().filter_map(|p| p.1.as_ref()).collect::<Vec<_>>();
255                        writer.try_write_param_table(
256                            CommentTag::Return,
257                            &returns,
258                            &item.comments,
259                        )?;
260
261                        writer.writeln()?;
262                    }
263
264                    ParseSource::Struct(ty) => {
265                        writer.write_section(&item.comments, &item.code)?;
266                        writer.try_write_properties_table(&ty.fields, &item.comments)?;
267                    }
268                    ParseSource::Event(ev) => {
269                        writer.write_section(&item.comments, &item.code)?;
270                        writer.try_write_events_table(&ev.fields, &item.comments)?;
271                    }
272                    ParseSource::Error(err) => {
273                        writer.write_section(&item.comments, &item.code)?;
274                        writer.try_write_errors_table(&err.fields, &item.comments)?;
275                    }
276                    ParseSource::Variable(_) | ParseSource::Enum(_) | ParseSource::Type(_) => {
277                        writer.write_section(&item.comments, &item.code)?;
278                    }
279                }
280            }
281            DocumentContent::Empty => (),
282        };
283
284        Ok(writer.finish())
285    }
286}
287
288impl Document {
289    /// Where all the source files are written to
290    fn target_src_dir(&self) -> PathBuf {
291        self.out_target_dir.join("src")
292    }
293
294    /// Writes a function to the buffer.
295    fn write_function(
296        &self,
297        writer: &mut BufWriter,
298        func: &FunctionDefinition,
299        comments: &Comments,
300        code: &str,
301    ) -> Result<(), std::fmt::Error> {
302        let func_name = func.name.as_ref().map_or(func.ty.to_string(), |n| n.name.to_owned());
303        let comments =
304            comments.merge_inheritdoc(&func_name, read_context!(self, INHERITDOC_ID, Inheritdoc));
305
306        // Write function name
307        writer.write_heading(&func_name)?;
308
309        writer.writeln()?;
310
311        // Write function docs
312        writer.writeln_doc(&comments.exclude_tags(&[CommentTag::Param, CommentTag::Return]))?;
313
314        // Write function header
315        writer.write_code(code)?;
316
317        // Write function parameter comments in a table
318        let params = func.params.iter().filter_map(|p| p.1.as_ref()).collect::<Vec<_>>();
319        writer.try_write_param_table(CommentTag::Param, &params, &comments)?;
320
321        // Write function return parameter comments in a table
322        let returns = func.returns.iter().filter_map(|p| p.1.as_ref()).collect::<Vec<_>>();
323        writer.try_write_param_table(CommentTag::Return, &returns, &comments)?;
324
325        writer.writeln()?;
326        Ok(())
327    }
328}