1use crate::{
4 hir_ext::{self, NameToPage, clean_block_doc_content},
5 utils::Deployment,
6};
7use solar::{
8 ast::{
9 CommentKind, ContractKind, DocComments, FunctionKind, ItemContract, ItemEnum, ItemError,
10 ItemEvent, ItemFunction, ItemKind, ItemStruct, ItemUdvt, NatSpecKind, ParameterList,
11 SourceUnit, Span, VariableDefinition,
12 },
13 interface::{
14 Ident,
15 source_map::{FileName, SourceFile, SourceMap},
16 },
17 sema::{Gcx, hir},
18};
19use std::{
20 collections::HashMap,
21 fmt::Write as _,
22 path::{Path, PathBuf},
23 sync::Arc,
24};
25
26#[allow(clippy::too_many_arguments)]
30pub fn source<'ast, 'gcx>(
31 ast: &'ast SourceUnit<'ast>,
32 file: &Arc<SourceFile>,
33 _sm: &SourceMap,
34 rel_sol_path: &Path,
35 abs_sol_path: &Path,
36 _root: &Path,
37 gcx: Gcx<'gcx>,
38 name_to_page: &NameToPage,
39 git_url: Option<&str>,
40 deployments: &HashMap<String, Vec<Deployment>>,
41) -> Vec<(PathBuf, String)> {
42 let out_dir = rel_sol_path.parent().unwrap_or(Path::new(""));
43 let stem = rel_sol_path.file_stem().and_then(|s| s.to_str()).unwrap_or("constants");
44
45 let src_text = file.src.as_str();
46 let src_start = file.start_pos.to_usize();
47 let ctx = Ctx { src_text, src_start };
48
49 let mut pages: Vec<(PathBuf, String)> = Vec::new();
50 let mut const_vars: Vec<(Span, &VariableDefinition<'_>, &DocComments<'_>)> = Vec::new();
51 let mut free_fns: std::collections::BTreeMap<
52 String,
53 Vec<(Span, &ItemFunction<'_>, &DocComments<'_>)>,
54 > = Default::default();
55
56 for item in ast.items.iter() {
57 let span = item.span;
58 match &item.kind {
59 ItemKind::Pragma(_) | ItemKind::Import(_) | ItemKind::Using(_) => (),
60 ItemKind::Contract(c) => {
61 let kind_str = contract_kind_str(c.kind);
62 let fname = format!("{kind_str}.{}.mdx", c.name.as_str());
63 let page_path = out_dir.join(&fname);
64 let hir_id = find_contract_id(gcx, c.name.as_str(), abs_sol_path);
66 let contract_deployments = if matches!(c.kind, ContractKind::Contract) {
68 deployments.get(c.name.as_str()).map(Vec::as_slice).unwrap_or(&[])
69 } else {
70 &[]
71 };
72 let content = render_contract(
73 span,
74 c,
75 &item.docs,
76 &ctx,
77 gcx,
78 hir_id,
79 name_to_page,
80 &page_path,
81 git_url,
82 contract_deployments,
83 );
84 pages.push((page_path, content));
85 }
86
87 ItemKind::Function(f) => {
88 let name = f.header.name.map(|n| n.as_str().to_string()).unwrap_or_default();
89 free_fns.entry(name).or_default().push((span, f, &item.docs));
90 }
91
92 ItemKind::Variable(v) => {
93 const_vars.push((span, v, &item.docs));
94 }
95
96 ItemKind::Struct(s) => {
97 let fname = format!("struct.{}.mdx", s.name.as_str());
98 let page_path = out_dir.join(&fname);
99 pages.push((
100 page_path.clone(),
101 render_struct(span, s, &item.docs, &ctx, name_to_page, &page_path, git_url),
102 ));
103 }
104
105 ItemKind::Enum(e) => {
106 let fname = format!("enum.{}.mdx", e.name.as_str());
107 let page_path = out_dir.join(&fname);
108 pages.push((
109 page_path.clone(),
110 render_enum(span, e, &item.docs, &ctx, name_to_page, &page_path, git_url),
111 ));
112 }
113
114 ItemKind::Udvt(u) => {
115 let fname = format!("type.{}.mdx", u.name.as_str());
116 let page_path = out_dir.join(&fname);
117 pages.push((
118 page_path.clone(),
119 render_udvt(span, u, &item.docs, &ctx, name_to_page, &page_path, git_url),
120 ));
121 }
122
123 ItemKind::Error(e) => {
124 let fname = format!("error.{}.mdx", e.name.as_str());
125 let page_path = out_dir.join(&fname);
126 pages.push((
127 page_path.clone(),
128 render_error(span, e, &item.docs, &ctx, name_to_page, &page_path, git_url),
129 ));
130 }
131
132 ItemKind::Event(e) => {
133 let fname = format!("event.{}.mdx", e.name.as_str());
134 let page_path = out_dir.join(&fname);
135 pages.push((
136 page_path.clone(),
137 render_event(span, e, &item.docs, &ctx, name_to_page, &page_path, git_url),
138 ));
139 }
140 }
141 }
142
143 for (name, overloads) in &free_fns {
144 let fname = format!("function.{name}.mdx");
145 let page_path = out_dir.join(&fname);
146 let content =
147 render_free_functions(name, overloads, &ctx, name_to_page, &page_path, git_url);
148 pages.push((page_path, content));
149 }
150
151 if !const_vars.is_empty() {
152 let fname = format!("constants.{stem}.mdx");
153 let page_path = out_dir.join(&fname);
154 let content = render_constants(stem, &const_vars, &ctx, name_to_page, &page_path, git_url);
155 pages.push((page_path, content));
156 }
157
158 pages
159}
160
161struct Ctx<'a> {
164 src_text: &'a str,
165 src_start: usize,
166}
167
168impl<'a> Ctx<'a> {
169 fn snippet(&self, span: Span) -> &'a str {
170 let lo = span.lo().to_usize().saturating_sub(self.src_start);
171 let hi = span.hi().to_usize().saturating_sub(self.src_start);
172 let lo = lo.min(self.src_text.len());
173 let hi = hi.min(self.src_text.len());
174 &self.src_text[lo..hi]
175 }
176
177 fn dedented_snippet(&self, span: Span) -> String {
178 dedent(self.snippet(span))
179 }
180}
181
182#[allow(clippy::too_many_arguments)]
185fn render_contract<'ast, 'gcx>(
186 _span: Span,
187 c: &'ast ItemContract<'ast>,
188 docs: &'ast DocComments<'ast>,
189 ctx: &Ctx<'_>,
190 gcx: Gcx<'gcx>,
191 hir_id: Option<hir::ContractId>,
192 name_to_page: &NameToPage,
193 page_path: &Path,
194 git_url: Option<&str>,
195 deployments: &[Deployment],
196) -> String {
197 let name = c.name.as_str();
198 let comments = collect_comments(docs, name_to_page, page_path);
199 let mut out = String::new();
200 write_frontmatter(&mut out, name, first_notice(&comments).as_deref());
201 writeln!(out, "# {name}").unwrap();
202 writeln!(out).unwrap();
203 write_git_source(&mut out, git_url);
204 write_deployments_table(&mut out, deployments);
205
206 if let Some(id) = hir_id
208 && let Some(inherits) = hir_ext::inheritance_links(gcx, id, name_to_page, page_path)
209 {
210 writeln!(out, "{inherits}").unwrap();
211 writeln!(out).unwrap();
212 }
213
214 write_comment_block(&mut out, &comments);
215
216 let mut constants: Vec<(Span, &VariableDefinition<'_>, &DocComments<'_>)> = Vec::new();
218 let mut state_vars: Vec<(Span, &VariableDefinition<'_>, &DocComments<'_>)> = Vec::new();
219 let mut functions: Vec<(Span, &ItemFunction<'_>, &DocComments<'_>)> = Vec::new();
220 let mut events: Vec<(Span, &ItemEvent<'_>, &DocComments<'_>)> = Vec::new();
221 let mut errors: Vec<(Span, &ItemError<'_>, &DocComments<'_>)> = Vec::new();
222 let mut structs: Vec<(Span, &ItemStruct<'_>, &DocComments<'_>)> = Vec::new();
223 let mut enums: Vec<(Span, &ItemEnum<'_>, &DocComments<'_>)> = Vec::new();
224 let mut udvts: Vec<(Span, &ItemUdvt<'_>, &DocComments<'_>)> = Vec::new();
225
226 for member in c.body.iter() {
227 let s = member.span;
228 match &member.kind {
229 ItemKind::Variable(v) => {
230 if v.mutability.is_some_and(|m| m.is_constant() || m.is_immutable()) {
232 constants.push((s, v, &member.docs));
233 } else {
234 state_vars.push((s, v, &member.docs));
235 }
236 }
237 ItemKind::Function(f) => functions.push((s, f, &member.docs)),
238 ItemKind::Event(e) => events.push((s, e, &member.docs)),
239 ItemKind::Error(e) => errors.push((s, e, &member.docs)),
240 ItemKind::Struct(st) => structs.push((s, st, &member.docs)),
241 ItemKind::Enum(e) => enums.push((s, e, &member.docs)),
242 ItemKind::Udvt(u) => udvts.push((s, u, &member.docs)),
243 _ => {}
244 }
245 }
246
247 let write_vars =
248 |out: &mut String, vars: &[(Span, &VariableDefinition<'_>, &DocComments<'_>)]| {
249 for (span, v, docs) in vars {
250 let vname = v.name.map(|n| n.as_str().to_string()).unwrap_or_default();
251 writeln!(out, "### {vname}").unwrap();
252 writeln!(out).unwrap();
253 let mut c = collect_comments(docs, name_to_page, page_path);
254 let inherited = inheritdoc_base(docs).and_then(|base| {
256 hir_id.and_then(|cid| hir_ext::resolve_inheritdoc_var(gcx, cid, &vname, &base))
257 });
258 if let Some(ref base_doc) = inherited {
259 let sanitize =
260 |s: &str| hir_ext::replace_inline_links(s, name_to_page, page_path);
261 if c.notices.is_empty() {
262 let inherited_notices: Vec<String> =
263 base_doc.notices.iter().map(|s| sanitize(s)).collect();
264 let mut new_desc: Vec<Description> = inherited_notices
265 .iter()
266 .map(|s| Description { kind: DescKind::Notice, content: s.clone() })
267 .collect();
268 new_desc.append(&mut c.descriptions);
269 c.descriptions = new_desc;
270 c.notices.extend(inherited_notices);
271 }
272 if c.devs.is_empty() {
273 let inherited_devs: Vec<String> =
274 base_doc.devs.iter().map(|s| sanitize(s)).collect();
275 c.descriptions.extend(
276 inherited_devs
277 .iter()
278 .map(|s| Description { kind: DescKind::Dev, content: s.clone() }),
279 );
280 c.devs.extend(inherited_devs);
281 }
282 for (name, desc) in &base_doc.returns {
283 if !c.returns.iter().any(|(n, _)| n == name) {
284 c.returns.push((name.clone(), sanitize(desc)));
285 }
286 }
287 }
288 write_comment_block(out, &c);
289 write_code_block(out, &ctx.dedented_snippet(*span));
290 if !c.returns.is_empty() {
291 let ty = format!("`{}`", ctx.snippet(v.ty.span).trim());
292 writeln!(out, "**Returns**").unwrap();
293 writeln!(out).unwrap();
294 writeln!(out, "| Name | Type | Description |").unwrap();
295 writeln!(out, "| ---- | ---- | ----------- |").unwrap();
296 for (name, desc) in &c.returns {
297 let full_desc =
301 if desc.is_empty() { name.clone() } else { format!("{name} {desc}") };
302 let desc_cell = escape_table_cell(&full_desc);
303 writeln!(out, "| <none> | {ty} | {desc_cell} |").unwrap();
304 }
305 writeln!(out).unwrap();
306 }
307 }
308 };
309
310 if !constants.is_empty() {
311 writeln!(out, "## Constants").unwrap();
312 writeln!(out).unwrap();
313 write_vars(&mut out, &constants);
314 }
315
316 if !state_vars.is_empty() {
317 writeln!(out, "## State Variables").unwrap();
318 writeln!(out).unwrap();
319 write_vars(&mut out, &state_vars);
320 }
321
322 if !functions.is_empty() {
323 writeln!(out, "## Functions").unwrap();
324 writeln!(out).unwrap();
325 for (span, f, docs) in &functions {
326 let fn_name = f.header.name.map(|n| n.as_str().to_string());
327 let inherited = fn_name.as_deref().and_then(|fname| {
329 let base = inheritdoc_base(docs)?;
330 let param_types = hir_ext::parameter_type_strings(gcx, &f.header.parameters);
331 hir_id.and_then(|cid| {
332 hir_ext::resolve_inheritdoc(gcx, cid, fname, &base, Some(¶m_types))
333 })
334 });
335 render_function_section(
336 &mut out,
337 *span,
338 f,
339 docs,
340 ctx,
341 name_to_page,
342 page_path,
343 inherited.as_ref(),
344 );
345 }
346 }
347
348 if !events.is_empty() {
349 writeln!(out, "## Events").unwrap();
350 writeln!(out).unwrap();
351 for (span, e, docs) in &events {
352 writeln!(out, "### {}", e.name.as_str()).unwrap();
353 writeln!(out).unwrap();
354 let c = collect_comments(docs, name_to_page, page_path);
355 write_comment_block(&mut out, &c);
356 write_code_block(&mut out, &ctx.dedented_snippet(*span));
357 write_param_table(&mut out, "Parameters", &e.parameters, &c, ctx);
358 }
359 }
360
361 if !errors.is_empty() {
362 writeln!(out, "## Errors").unwrap();
363 writeln!(out).unwrap();
364 for (span, e, docs) in &errors {
365 writeln!(out, "### {}", e.name.as_str()).unwrap();
366 writeln!(out).unwrap();
367 let c = collect_comments(docs, name_to_page, page_path);
368 write_comment_block(&mut out, &c);
369 write_code_block(&mut out, &ctx.dedented_snippet(*span));
370 write_param_table(&mut out, "Parameters", &e.parameters, &c, ctx);
371 }
372 }
373
374 if !structs.is_empty() {
375 writeln!(out, "## Structs").unwrap();
376 writeln!(out).unwrap();
377 for (span, s, docs) in &structs {
378 writeln!(out, "### {}", s.name.as_str()).unwrap();
379 writeln!(out).unwrap();
380 let c = collect_comments(docs, name_to_page, page_path);
381 write_comment_block(&mut out, &c);
382 write_code_block(&mut out, &ctx.dedented_snippet(*span));
383 write_struct_properties_table(&mut out, s.fields, &c, ctx);
384 }
385 }
386
387 if !enums.is_empty() {
388 writeln!(out, "## Enums").unwrap();
389 writeln!(out).unwrap();
390 for (span, e, docs) in &enums {
391 writeln!(out, "### {}", e.name.as_str()).unwrap();
392 writeln!(out).unwrap();
393 let c = collect_comments(docs, name_to_page, page_path);
394 write_comment_block(&mut out, &c);
395 write_code_block(&mut out, &ctx.dedented_snippet(*span));
396 write_enum_variants_table(&mut out, e.variants, &c);
397 }
398 }
399
400 if !udvts.is_empty() {
401 writeln!(out, "## Custom Types").unwrap();
402 writeln!(out).unwrap();
403 for (span, u, docs) in &udvts {
404 writeln!(out, "### {}", u.name.as_str()).unwrap();
405 writeln!(out).unwrap();
406 let c = collect_comments(docs, name_to_page, page_path);
407 write_comment_block(&mut out, &c);
408 write_code_block(&mut out, &format!("{};", ctx.dedented_snippet(*span)));
409 }
410 }
411
412 out
413}
414
415fn render_free_functions(
418 name: &str,
419 overloads: &[(Span, &ItemFunction<'_>, &DocComments<'_>)],
420 ctx: &Ctx<'_>,
421 name_to_page: &NameToPage,
422 page_path: &Path,
423 git_url: Option<&str>,
424) -> String {
425 let title = if name.is_empty() { "function" } else { name };
426 let first_comments = collect_comments(overloads[0].2, name_to_page, page_path);
427 let mut out = String::new();
428 write_frontmatter(&mut out, title, first_notice(&first_comments).as_deref());
429 writeln!(out, "# {title}").unwrap();
430 writeln!(out).unwrap();
431 write_git_source(&mut out, git_url);
432 for (span, f, docs) in overloads {
433 render_function_section(&mut out, *span, f, docs, ctx, name_to_page, page_path, None);
434 }
435 out
436}
437
438fn render_constants(
441 stem: &str,
442 vars: &[(Span, &VariableDefinition<'_>, &DocComments<'_>)],
443 ctx: &Ctx<'_>,
444 name_to_page: &NameToPage,
445 page_path: &Path,
446 git_url: Option<&str>,
447) -> String {
448 let title = format!("{stem} Constants");
449 let mut out = String::new();
450 write_frontmatter(&mut out, &title, None);
451 writeln!(out, "# {title}").unwrap();
452 writeln!(out).unwrap();
453 write_git_source(&mut out, git_url);
454 for (span, v, docs) in vars {
455 let name = v.name.map(|n| n.as_str().to_string()).unwrap_or_else(|| "_".to_string());
456 writeln!(out, "## {name}").unwrap();
457 writeln!(out).unwrap();
458 let c = collect_comments(docs, name_to_page, page_path);
459 write_comment_block(&mut out, &c);
460 write_code_block(&mut out, &ctx.dedented_snippet(*span));
461 }
462 out
463}
464
465fn render_struct<'ast>(
468 span: Span,
469 s: &'ast ItemStruct<'ast>,
470 docs: &'ast DocComments<'ast>,
471 ctx: &Ctx<'_>,
472 name_to_page: &NameToPage,
473 page_path: &Path,
474 git_url: Option<&str>,
475) -> String {
476 let name = s.name.as_str();
477 let c = collect_comments(docs, name_to_page, page_path);
478 let mut out = String::new();
479 write_frontmatter(&mut out, name, first_notice(&c).as_deref());
480 writeln!(out, "# {name}").unwrap();
481 writeln!(out).unwrap();
482 write_git_source(&mut out, git_url);
483 write_comment_block(&mut out, &c);
484 write_code_block(&mut out, &ctx.dedented_snippet(span));
485 write_struct_properties_table(&mut out, s.fields, &c, ctx);
486 out
487}
488
489fn render_enum<'ast>(
490 span: Span,
491 e: &'ast ItemEnum<'ast>,
492 docs: &'ast DocComments<'ast>,
493 ctx: &Ctx<'_>,
494 name_to_page: &NameToPage,
495 page_path: &Path,
496 git_url: Option<&str>,
497) -> String {
498 let name = e.name.as_str();
499 let c = collect_comments(docs, name_to_page, page_path);
500 let mut out = String::new();
501 write_frontmatter(&mut out, name, first_notice(&c).as_deref());
502 writeln!(out, "# {name}").unwrap();
503 writeln!(out).unwrap();
504 write_git_source(&mut out, git_url);
505 write_comment_block(&mut out, &c);
506 write_code_block(&mut out, &ctx.dedented_snippet(span));
507 write_enum_variants_table(&mut out, e.variants, &c);
508 out
509}
510
511fn render_udvt<'ast>(
512 span: Span,
513 u: &'ast ItemUdvt<'ast>,
514 docs: &'ast DocComments<'ast>,
515 ctx: &Ctx<'_>,
516 name_to_page: &NameToPage,
517 page_path: &Path,
518 git_url: Option<&str>,
519) -> String {
520 let name = u.name.as_str();
521 let c = collect_comments(docs, name_to_page, page_path);
522 let mut out = String::new();
523 write_frontmatter(&mut out, name, first_notice(&c).as_deref());
524 writeln!(out, "# {name}").unwrap();
525 writeln!(out).unwrap();
526 write_git_source(&mut out, git_url);
527 write_comment_block(&mut out, &c);
528 write_code_block(&mut out, &format!("{};", ctx.dedented_snippet(span)));
529 out
530}
531
532fn render_error<'ast>(
533 span: Span,
534 e: &'ast ItemError<'ast>,
535 docs: &'ast DocComments<'ast>,
536 ctx: &Ctx<'_>,
537 name_to_page: &NameToPage,
538 page_path: &Path,
539 git_url: Option<&str>,
540) -> String {
541 let name = e.name.as_str();
542 let c = collect_comments(docs, name_to_page, page_path);
543 let mut out = String::new();
544 write_frontmatter(&mut out, name, first_notice(&c).as_deref());
545 writeln!(out, "# {name}").unwrap();
546 writeln!(out).unwrap();
547 write_git_source(&mut out, git_url);
548 write_comment_block(&mut out, &c);
549 write_code_block(&mut out, &ctx.dedented_snippet(span));
550 write_param_table(&mut out, "Parameters", &e.parameters, &c, ctx);
551 out
552}
553
554fn render_event<'ast>(
555 span: Span,
556 e: &'ast ItemEvent<'ast>,
557 docs: &'ast DocComments<'ast>,
558 ctx: &Ctx<'_>,
559 name_to_page: &NameToPage,
560 page_path: &Path,
561 git_url: Option<&str>,
562) -> String {
563 let name = e.name.as_str();
564 let c = collect_comments(docs, name_to_page, page_path);
565 let mut out = String::new();
566 write_frontmatter(&mut out, name, first_notice(&c).as_deref());
567 writeln!(out, "# {name}").unwrap();
568 writeln!(out).unwrap();
569 write_git_source(&mut out, git_url);
570 write_comment_block(&mut out, &c);
571 write_code_block(&mut out, &ctx.dedented_snippet(span));
572 write_param_table(&mut out, "Parameters", &e.parameters, &c, ctx);
573 out
574}
575
576#[allow(clippy::too_many_arguments)]
578fn render_function_section(
579 out: &mut String,
580 span: Span,
581 f: &ItemFunction<'_>,
582 docs: &DocComments<'_>,
583 ctx: &Ctx<'_>,
584 name_to_page: &NameToPage,
585 page_path: &Path,
586 inherited: Option<&hir_ext::InheritedDoc>,
587) {
588 let heading = function_heading(f);
589 if let Some(anchor) = function_signature_anchor(f, ctx) {
590 writeln!(out, "<a id=\"{anchor}\"></a>").unwrap();
591 writeln!(out).unwrap();
592 }
593 writeln!(out, "### {heading}").unwrap();
594 writeln!(out).unwrap();
595 let mut c = collect_comments(docs, name_to_page, page_path);
596 if let Some(inherited) = inherited {
598 let sanitize = |s: &str| hir_ext::replace_inline_links(s, name_to_page, page_path);
599 let inherited_notices: Vec<String> =
600 inherited.notices.iter().map(|s| sanitize(s)).collect();
601 let inherited_devs: Vec<String> = inherited.devs.iter().map(|s| sanitize(s)).collect();
602 if c.notices.is_empty() {
603 let mut new_desc: Vec<Description> = inherited_notices
604 .iter()
605 .map(|s| Description { kind: DescKind::Notice, content: s.clone() })
606 .collect();
607 new_desc.append(&mut c.descriptions);
608 c.descriptions = new_desc;
609 c.notices.extend_from_slice(&inherited_notices);
610 }
611 if c.devs.is_empty() {
612 c.devs.extend_from_slice(&inherited_devs);
613 c.descriptions.extend(
614 inherited_devs
615 .iter()
616 .map(|s| Description { kind: DescKind::Dev, content: s.clone() }),
617 );
618 }
619 for (name, desc) in &inherited.params {
620 if !c.params.iter().any(|(n, _)| n == name) {
621 c.params.push((name.clone(), sanitize(desc)));
622 }
623 }
624 for (name, desc) in &inherited.returns {
625 if !c.returns.iter().any(|(n, _)| n == name) {
626 c.returns.push((name.clone(), sanitize(desc)));
627 }
628 }
629 }
630 write_comment_block(out, &c);
631 let hspan = if f.header.span.lo() == f.header.span.hi() { span } else { f.header.span };
632 let snippet = ctx.dedented_snippet(hspan);
633 write_code_block(out, &format!("{snippet};"));
634 write_param_table(out, "Parameters", &f.header.parameters, &c, ctx);
635 if let Some(returns) = &f.header.returns {
636 write_param_table(out, "Returns", returns, &c, ctx);
637 }
638}
639
640fn function_heading(f: &ItemFunction<'_>) -> String {
641 match f.kind {
642 FunctionKind::Constructor => "constructor".to_string(),
643 FunctionKind::Fallback => "fallback".to_string(),
644 FunctionKind::Receive => "receive".to_string(),
645 FunctionKind::Function | FunctionKind::Modifier => {
646 f.header.name.map(|n| n.as_str().to_string()).unwrap_or_else(|| "function".to_string())
647 }
648 }
649}
650
651fn function_signature_anchor(f: &ItemFunction<'_>, ctx: &Ctx<'_>) -> Option<String> {
652 let name = match f.kind {
653 FunctionKind::Constructor => "constructor".to_string(),
654 FunctionKind::Fallback => "fallback".to_string(),
655 FunctionKind::Receive => "receive".to_string(),
656 FunctionKind::Function | FunctionKind::Modifier => f.header.name?.as_str().to_string(),
657 };
658 let params = f
659 .header
660 .parameters
661 .vars
662 .iter()
663 .map(|v| ctx.snippet(v.ty.span).trim().to_string())
664 .collect::<Vec<_>>();
665
666 Some(hir_ext::function_signature_anchor(&name, ¶ms))
667}
668
669#[derive(Clone, Copy, PartialEq, Eq)]
672enum DescKind {
673 Notice,
674 Dev,
675}
676
677struct Description {
678 kind: DescKind,
679 content: String,
680}
681
682struct CommentData {
683 titles: Vec<String>,
684 authors: Vec<String>,
685 notices: Vec<String>,
687 devs: Vec<String>,
689 descriptions: Vec<Description>,
693 params: Vec<(String, String)>,
694 returns: Vec<(String, String)>,
695 customs: Vec<(String, String)>,
696 unnamed_param_names: Vec<String>,
698}
699
700fn collect_comments(
707 docs: &DocComments<'_>,
708 name_to_page: &NameToPage,
709 page_path: &Path,
710) -> CommentData {
711 let mut data = CommentData {
712 titles: Vec::new(),
713 authors: Vec::new(),
714 notices: Vec::new(),
715 devs: Vec::new(),
716 descriptions: Vec::new(),
717 params: Vec::new(),
718 returns: Vec::new(),
719 customs: Vec::new(),
720 unnamed_param_names: Vec::new(),
721 };
722
723 const FILTERED_CUSTOM: &[&str] = &["solidity", "src", "use-src", "ast-id"];
725 const KNOWN_CUSTOM: &[&str] = &["name"];
727
728 let mut prev_doc_was_blank = false;
731 #[derive(Clone, Copy)]
732 enum LastSection {
733 Desc, Param,
735 Return,
736 }
737 let mut last_section: Option<LastSection> = None;
738
739 for doc in docs.iter() {
740 if doc.natspec.is_empty() {
741 prev_doc_was_blank = true;
742 continue;
743 }
744
745 for item in doc.natspec.iter() {
746 let raw = doc.natspec_content(item);
747 let raw: &str =
750 if doc.kind == CommentKind::Block { &clean_block_doc_content(raw) } else { raw };
751
752 let is_continuation = matches!(item.kind, NatSpecKind::Notice)
756 && raw.starts_with(|c: char| c.is_whitespace());
757
758 let trimmed = raw.trim();
759 if trimmed.is_empty() {
760 prev_doc_was_blank = true;
761 continue;
762 }
763
764 let content = hir_ext::replace_inline_links(trimmed, name_to_page, page_path);
766
767 if is_continuation && !prev_doc_was_blank {
768 let appended = match last_section {
769 Some(LastSection::Desc) => data.descriptions.last_mut().map(|d| &mut d.content),
770 Some(LastSection::Param) => data.params.last_mut().map(|(_, d)| d),
771 Some(LastSection::Return) => data.returns.last_mut().map(|(_, d)| d),
772 None => None,
773 };
774 if let Some(last) = appended {
775 last.push('\n');
776 last.push_str(&content);
777 prev_doc_was_blank = false;
778 continue;
779 }
780 }
781
782 prev_doc_was_blank = false;
783
784 match item.kind {
785 NatSpecKind::Title => data.titles.push(content),
786 NatSpecKind::Author => data.authors.push(content),
787 NatSpecKind::Notice => {
788 data.notices.push(content.clone());
789 data.descriptions.push(Description { kind: DescKind::Notice, content });
790 last_section = Some(LastSection::Desc);
791 }
792 NatSpecKind::Dev => {
793 data.devs.push(content.clone());
794 data.descriptions.push(Description { kind: DescKind::Dev, content });
795 last_section = Some(LastSection::Desc);
796 }
797 NatSpecKind::Param { name } => {
798 data.params.push((name.as_str().to_string(), content));
799 last_section = Some(LastSection::Param);
800 }
801 NatSpecKind::Return { name } => {
802 data.returns.push((
803 name.map(|name| name.as_str().to_string()).unwrap_or_default(),
804 content,
805 ));
806 last_section = Some(LastSection::Return);
807 }
808 NatSpecKind::Inheritdoc { .. } => {} NatSpecKind::Custom { name } => {
810 let tag = name.as_str();
811 if FILTERED_CUSTOM.contains(&tag) {
812 } else if tag == "name" {
814 if let Some(first) = content.split_whitespace().next() {
816 data.unnamed_param_names.push(first.to_string());
817 }
818 } else {
819 if !KNOWN_CUSTOM.contains(&tag) && !is_known_custom_tag(tag) {
821 warn!("unknown natspec custom tag: @custom:{tag}");
822 }
823 data.customs.push((tag.to_string(), content));
824 }
825 }
826 NatSpecKind::Internal { .. } => {}
827 }
828 }
829 }
830
831 data
832}
833
834fn is_known_custom_tag(tag: &str) -> bool {
839 !tag.is_empty() && tag.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
840}
841
842fn inheritdoc_base(docs: &DocComments<'_>) -> Option<String> {
844 for doc in docs.iter() {
845 for item in doc.natspec.iter() {
846 if let NatSpecKind::Inheritdoc { contract } = item.kind {
847 return Some(contract.as_str().to_string());
848 }
849 }
850 }
851 None
852}
853
854fn first_notice(data: &CommentData) -> Option<String> {
855 data.notices.first().cloned()
856}
857
858fn write_frontmatter(out: &mut String, title: &str, description: Option<&str>) {
861 writeln!(out, "---").unwrap();
862 writeln!(out, "title: \"{}\"", yaml_escape_double_quoted(title)).unwrap();
863 if let Some(desc) = description {
864 let collapsed: String = desc.split_whitespace().collect::<Vec<_>>().join(" ");
866 writeln!(out, "description: \"{}\"", yaml_escape_double_quoted(&collapsed)).unwrap();
867 }
868 writeln!(out, "---").unwrap();
869 writeln!(out).unwrap();
870}
871
872fn yaml_escape_double_quoted(s: &str) -> String {
878 let mut out = String::with_capacity(s.len());
879 for c in s.chars() {
880 match c {
881 '\\' => out.push_str("\\\\"),
882 '"' => out.push_str("\\\""),
883 '\n' => out.push_str("\\n"),
884 '\r' => out.push_str("\\r"),
885 '\t' => out.push_str("\\t"),
886 '\u{0}' => out.push_str("\\0"),
887 c if (c as u32) < 0x20 => {
888 out.push_str(&format!("\\x{:02x}", c as u32));
889 }
890 c => out.push(c),
891 }
892 }
893 out
894}
895
896fn italicize_dev(content: &str) -> String {
900 let trimmed = content.trim_matches('\n');
901 if trimmed.is_empty() { String::new() } else { format!("<i>\n\n{trimmed}\n\n</i>") }
902}
903
904fn write_comment_block(out: &mut String, data: &CommentData) {
905 if !data.titles.is_empty() {
906 let label = if data.titles.len() == 1 { "Title" } else { "Titles" };
907 writeln!(out, "**{label}:** {}", data.titles.join(", ")).unwrap();
908 writeln!(out).unwrap();
909 }
910 if !data.authors.is_empty() {
911 let label = if data.authors.len() == 1 { "Author" } else { "Authors" };
912 writeln!(out, "**{label}:** {}", data.authors.join(", ")).unwrap();
913 writeln!(out).unwrap();
914 }
915 for desc in &data.descriptions {
919 match desc.kind {
920 DescKind::Notice => writeln!(out, "{}", desc.content).unwrap(),
921 DescKind::Dev => writeln!(out, "{}", italicize_dev(&desc.content)).unwrap(),
922 }
923 writeln!(out).unwrap();
924 }
925 if !data.customs.is_empty() {
926 let label = if data.customs.len() == 1 { "Note" } else { "Notes" };
927 writeln!(out, "**{label}:**").unwrap();
928 writeln!(out).unwrap();
929 for (tag, content) in &data.customs {
930 writeln!(out, "- **{tag}:** {content}").unwrap();
931 }
932 writeln!(out).unwrap();
933 }
934}
935
936fn write_code_block(out: &mut String, snippet: &str) {
937 writeln!(out, "```solidity").unwrap();
938 writeln!(out, "{}", snippet.trim_end()).unwrap();
939 writeln!(out, "```").unwrap();
940 writeln!(out).unwrap();
941}
942
943fn write_git_source(out: &mut String, git_url: Option<&str>) {
945 if let Some(url) = git_url {
946 writeln!(out, "[Git Source]({url})").unwrap();
947 writeln!(out).unwrap();
948 }
949}
950
951fn escape_table_cell(s: &str) -> String {
956 s.replace('\\', "\\\\")
957 .replace('|', "\\|")
958 .replace("\r\n", "<br/>")
959 .replace(['\n', '\r'], "<br/>")
960}
961
962fn write_deployments_table(out: &mut String, deployments: &[Deployment]) {
964 if deployments.is_empty() {
965 return;
966 }
967 writeln!(out, "**Deployments**").unwrap();
968 writeln!(out).unwrap();
969 writeln!(out, "| Network | Address |").unwrap();
970 writeln!(out, "| ------- | ------- |").unwrap();
971 for d in deployments {
972 let network = escape_table_cell(d.network.as_deref().unwrap_or("-"));
973 writeln!(out, "| {network} | `{:#x}` |", d.address).unwrap();
974 }
975 writeln!(out).unwrap();
976}
977
978fn write_param_table(
979 out: &mut String,
980 heading: &str,
981 params: &ParameterList<'_>,
982 comments: &CommentData,
983 ctx: &Ctx<'_>,
984) {
985 if params.is_empty() {
986 return;
987 }
988 writeln!(out, "**{heading}**").unwrap();
989 writeln!(out).unwrap();
990 writeln!(out, "| Name | Type | Description |").unwrap();
991 writeln!(out, "| ---- | ---- | ----------- |").unwrap();
992 let is_return = heading == "Returns";
993 let mut unnamed_iter = comments.unnamed_param_names.iter();
996 for (index, var) in params.iter().enumerate() {
997 let name = match var.name {
998 Some(n) => n.as_str().to_string(),
999 None if !is_return => unnamed_iter.next().cloned().unwrap_or_else(|| "_".to_string()),
1000 None => "<none>".to_string(),
1001 };
1002 let ty = format!("`{}`", ctx.snippet(var.ty.span).trim());
1003 let desc = if is_return {
1004 return_description(comments, index, var.name.map(|_| name.as_str()))
1005 } else {
1006 comments.params.iter().find(|(n, _)| n == &name).map(|(_, d)| d.as_str()).unwrap_or("")
1007 };
1008 let name = escape_table_cell(&name);
1009 let desc = escape_table_cell(desc);
1010 writeln!(out, "| {name} | {ty} | {desc} |").unwrap();
1011 }
1012 writeln!(out).unwrap();
1013}
1014
1015fn return_description<'a>(
1016 comments: &'a CommentData,
1017 index: usize,
1018 return_name: Option<&str>,
1019) -> &'a str {
1020 if let Some(return_name) = return_name
1021 && let Some((_, desc)) = comments.returns.iter().find(|(n, _)| n == return_name)
1022 {
1023 return desc;
1024 }
1025
1026 let Some((doc_name, desc)) = comments.returns.get(index) else {
1027 return "";
1028 };
1029
1030 if !doc_name.is_empty() {
1031 return desc;
1032 }
1033
1034 match return_name {
1035 Some(return_name) => desc.strip_prefix(return_name).and_then(strip_one_ws).unwrap_or(desc),
1036 None => split_first_word(desc).map(|(_, desc)| desc).unwrap_or(desc),
1037 }
1038}
1039
1040fn strip_one_ws(s: &str) -> Option<&str> {
1041 let mut chars = s.char_indices();
1042 let (_, first) = chars.next()?;
1043 first.is_whitespace().then(|| chars.next().map(|(idx, _)| &s[idx..]).unwrap_or(""))
1044}
1045
1046fn split_first_word(s: &str) -> Option<(&str, &str)> {
1047 let trimmed = s.trim_start();
1048 let split = trimmed.find(char::is_whitespace)?;
1049 let (first, rest) = trimmed.split_at(split);
1050 Some((first, rest.trim_start()))
1051}
1052
1053fn write_struct_properties_table(
1054 out: &mut String,
1055 fields: &[VariableDefinition<'_>],
1056 comments: &CommentData,
1057 ctx: &Ctx<'_>,
1058) {
1059 if fields.is_empty() {
1060 return;
1061 }
1062 writeln!(out, "**Properties**").unwrap();
1063 writeln!(out).unwrap();
1064 writeln!(out, "| Name | Type | Description |").unwrap();
1065 writeln!(out, "| ---- | ---- | ----------- |").unwrap();
1066 for field in fields {
1067 let name = field.name.map(|n| n.as_str().to_string()).unwrap_or_else(|| "_".to_string());
1068 let ty = format!("`{}`", ctx.snippet(field.ty.span).trim());
1069 let desc =
1070 comments.params.iter().find(|(n, _)| n == &name).map(|(_, d)| d.as_str()).unwrap_or("");
1071 let name = escape_table_cell(&name);
1072 let desc = escape_table_cell(desc);
1073 writeln!(out, "| {name} | {ty} | {desc} |").unwrap();
1074 }
1075 writeln!(out).unwrap();
1076}
1077
1078fn write_enum_variants_table(out: &mut String, variants: &[Ident], comments: &CommentData) {
1079 if variants.is_empty() {
1080 return;
1081 }
1082 writeln!(out, "**Variants**").unwrap();
1083 writeln!(out).unwrap();
1084 writeln!(out, "| Name | Description |").unwrap();
1085 writeln!(out, "| ---- | ----------- |").unwrap();
1086 for variant in variants {
1087 let name = variant.as_str();
1088 let desc =
1089 comments.params.iter().find(|(n, _)| n == name).map(|(_, d)| d.as_str()).unwrap_or("");
1090 let name = escape_table_cell(name);
1091 let desc = escape_table_cell(desc);
1092 writeln!(out, "| {name} | {desc} |").unwrap();
1093 }
1094 writeln!(out).unwrap();
1095}
1096
1097const fn contract_kind_str(kind: ContractKind) -> &'static str {
1098 match kind {
1099 ContractKind::Contract => "contract",
1100 ContractKind::AbstractContract => "abstract",
1101 ContractKind::Interface => "interface",
1102 ContractKind::Library => "library",
1103 }
1104}
1105
1106fn find_contract_id<'gcx>(
1110 gcx: Gcx<'gcx>,
1111 name: &str,
1112 abs_sol_path: &Path,
1113) -> Option<hir::ContractId> {
1114 gcx.hir.contract_ids().find(|&id| {
1115 let c = gcx.hir.contract(id);
1116 if c.name.as_str() != name {
1117 return false;
1118 }
1119 match &gcx.hir.source(c.source).file.name {
1120 FileName::Real(p) => p == abs_sol_path,
1121 _ => false,
1122 }
1123 })
1124}
1125
1126fn dedent(s: &str) -> String {
1128 let lines: Vec<&str> = s.lines().collect();
1129 if lines.is_empty() {
1130 return s.to_string();
1131 }
1132 let indent = lines
1133 .iter()
1134 .filter(|l| !l.trim().is_empty())
1135 .map(|l| l.len() - l.trim_start().len())
1136 .min()
1137 .unwrap_or(0);
1138 lines
1139 .iter()
1140 .map(|l| if l.len() >= indent { &l[indent..] } else { l.trim() })
1141 .collect::<Vec<_>>()
1142 .join("\n")
1143}