Skip to main content

forge_doc/writer/
as_doc.rs

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