Represent module level doc comments in forc doc (#4096)

## Description

Adds module level docs & previews to `forc doc`.

[Screencast from 2023-02-20
20-30-24.webm](https://user-images.githubusercontent.com/57543709/220232486-7ec0742a-59ef-41c9-a459-3c8c9b7d42c7.webm)


## Checklist

- [x] I have linked to any relevant issues. Closes #4095 
- [x] I have commented my code, particularly in hard-to-understand
areas.
- [x] I have updated the documentation where relevant (API docs, the
reference, and the Sway book).
- [x] I have added tests that prove my fix is effective or that my
feature works.
- [x] I have added (or requested a maintainer to add) the necessary
`Breaking*` or `New Feature` labels where relevant.
- [x] I have done my best to ensure that my PR adheres to [the Fuel Labs
Code Review
Standards](https://github.com/FuelLabs/rfcs/blob/master/text/code-standards/external-contributors.md).
- [x] I have requested a review from the relevant team or maintainers.
This commit is contained in:
Chris O'Brien 2023-02-21 21:02:14 -06:00 committed by GitHub
parent 5d2b10bd83
commit d9d5cb15a2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 131 additions and 72 deletions

View file

@ -1,6 +1,6 @@
use crate::{
descriptor::Descriptor,
render::{split_at_markdown_header, DocLink, ItemBody, ItemHeader, Renderable},
render::{split_at_markdown_header, DocLink, DocStrings, ItemBody, ItemHeader, Renderable},
};
use anyhow::Result;
use horrorshow::{box_html, RenderBox};
@ -50,35 +50,7 @@ impl Document {
}
}
fn preview_opt(&self) -> Option<String> {
const MAX_PREVIEW_CHARS: usize = 100;
const CLOSING_PARAGRAPH_TAG: &str = "</p>";
self.raw_attributes.as_ref().map(|description| {
let preview = split_at_markdown_header(description);
if preview.chars().count() > MAX_PREVIEW_CHARS
&& preview.contains(CLOSING_PARAGRAPH_TAG)
{
match preview.find(CLOSING_PARAGRAPH_TAG) {
Some(index) => {
// We add 1 here to get the index of the char after the closing tag.
// This ensures we retain the closing tag and don't break the html.
let (preview, _) =
preview.split_at(index + CLOSING_PARAGRAPH_TAG.len() + 1);
if preview.chars().count() > MAX_PREVIEW_CHARS && preview.contains('\n') {
match preview.find('\n') {
Some(index) => preview.split_at(index).0.to_string(),
None => unreachable!("Previous logic prevents this panic"),
}
} else {
preview.to_string()
}
}
None => unreachable!("Previous logic prevents this panic"),
}
} else {
preview.to_string()
}
})
create_preview(self.raw_attributes.clone())
}
/// Gather [Documentation] from the [TyProgram].
pub(crate) fn from_ty_program(
@ -95,7 +67,7 @@ impl Document {
let desc = Descriptor::from_typed_decl(
decl_engine,
decl,
ModuleInfo::from_vec(vec![project_name.to_owned()]),
ModuleInfo::from_ty_module(vec![project_name.to_owned()], None),
document_private_items,
)?;
@ -108,7 +80,10 @@ impl Document {
if !no_deps && !typed_program.root.submodules.is_empty() {
// this is the same process as before but for dependencies
for (_, ref typed_submodule) in &typed_program.root.submodules {
let module_prefix = ModuleInfo::from_vec(vec![project_name.to_owned()]);
let attributes = (!typed_submodule.module.attributes.is_empty())
.then(|| typed_submodule.module.attributes.to_html_string());
let module_prefix =
ModuleInfo::from_ty_module(vec![project_name.to_owned()], attributes);
Document::from_ty_submodule(
decl_engine,
typed_submodule,
@ -130,7 +105,7 @@ impl Document {
) -> Result<()> {
let mut new_submodule_prefix = module_prefix.to_owned();
new_submodule_prefix
.0
.module_prefixes
.push(typed_submodule.library_name.as_str().to_owned());
for ast_node in &typed_submodule.module.all_nodes {
if let TyAstNodeContent::Declaration(ref decl) = ast_node.content {
@ -173,13 +148,16 @@ impl Renderable for Document {
pub(crate) type ModulePrefix = String;
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub(crate) struct ModuleInfo(pub(crate) Vec<ModulePrefix>);
pub(crate) struct ModuleInfo {
pub(crate) module_prefixes: Vec<ModulePrefix>,
pub(crate) attributes: Option<String>,
}
impl ModuleInfo {
/// The current module.
///
/// Panics if there are no modules.
pub(crate) fn location(&self) -> &str {
self.0
self.module_prefixes
.last()
.expect("Expected Some module location, found None")
}
@ -187,7 +165,7 @@ impl ModuleInfo {
///
/// Panics if the project root is missing.
pub(crate) fn project_name(&self) -> &str {
self.0
self.module_prefixes
.first()
.expect("Expected root module, project root missing")
}
@ -197,7 +175,7 @@ impl ModuleInfo {
pub(crate) fn parent(&self) -> Option<&String> {
match self.has_parent() {
true => {
let mut iter = self.0.iter();
let mut iter = self.module_prefixes.iter();
iter.next_back();
iter.next_back()
}
@ -226,7 +204,7 @@ impl ModuleInfo {
///
/// Example: `module::submodule`
fn to_path_literal_prefix(&self, location: &str) -> String {
let mut iter = self.0.iter();
let mut iter = self.module_prefixes.iter();
for prefix in iter.by_ref() {
if prefix == location {
break;
@ -239,7 +217,7 @@ impl ModuleInfo {
///
/// This is only used for full path syntax, e.g `module/submodule/file_name.html`.
pub(crate) fn to_file_path_string(&self, file_name: &str, location: &str) -> Result<String> {
let mut iter = self.0.iter();
let mut iter = self.module_prefixes.iter();
for prefix in iter.by_ref() {
if prefix == location {
break;
@ -265,12 +243,47 @@ impl ModuleInfo {
}
/// The depth of a module as `usize`.
pub(crate) fn depth(&self) -> usize {
self.0.len()
self.module_prefixes.len()
}
/// Create a new [ModuleInfo] from a vec.
pub(crate) fn from_vec(vec: Vec<String>) -> Self {
Self(vec)
pub(crate) fn from_ty_module(module_prefixes: Vec<String>, attributes: Option<String>) -> Self {
Self {
module_prefixes,
attributes,
}
}
pub(crate) fn preview_opt(&self) -> Option<String> {
create_preview(self.attributes.clone())
}
}
/// Create a docstring preview from raw html attributes.
///
/// Returns `None` if there are no attributes.
fn create_preview(raw_attributes: Option<String>) -> Option<String> {
const MAX_PREVIEW_CHARS: usize = 100;
const CLOSING_PARAGRAPH_TAG: &str = "</p>";
raw_attributes.as_ref().map(|description| {
let preview = split_at_markdown_header(description);
if preview.chars().count() > MAX_PREVIEW_CHARS && preview.contains(CLOSING_PARAGRAPH_TAG) {
let closing_tag_index = preview
.find(CLOSING_PARAGRAPH_TAG)
.expect("closing tag out of range");
// We add 1 here to get the index of the char after the closing tag.
// This ensures we retain the closing tag and don't break the html.
let (preview, _) =
preview.split_at(closing_tag_index + CLOSING_PARAGRAPH_TAG.len() + 1);
if preview.chars().count() > MAX_PREVIEW_CHARS && preview.contains('\n') {
let newline_index = preview.find('\n').expect("new line char out of range");
preview.split_at(newline_index).0.to_string()
} else {
preview.to_string()
}
} else {
preview.to_string()
}
})
}
#[cfg(test)]
@ -283,12 +296,12 @@ mod tests {
let module = String::from("module_name");
let mut module_vec = vec![project.clone(), module];
let module_info = ModuleInfo::from_vec(module_vec.clone());
let module_info = ModuleInfo::from_ty_module(module_vec.clone(), None);
let project_opt = module_info.parent();
assert_eq!(Some(&project), project_opt);
module_vec.pop();
let module_info = ModuleInfo::from_vec(module_vec);
let module_info = ModuleInfo::from_ty_module(module_vec, None);
let project_opt = module_info.parent();
assert_eq!(None, project_opt);
}

View file

@ -82,18 +82,22 @@ pub fn main() -> Result<()> {
no_deps,
document_private_items,
)?;
let root_attributes =
(!typed_program.root.attributes.is_empty()).then_some(typed_program.root.attributes);
let program_kind = typed_program.kind;
// render docs to HTML
let forc_version = pkg_manifest
.project
.forc_version
.as_ref()
.map(|ver| format!("{}.{}.{}", ver.major, ver.minor, ver.patch));
let rendered_docs = RenderedDocumentation::from(raw_docs, forc_version)?;
let rendered_docs =
RenderedDocumentation::from(raw_docs, root_attributes, program_kind, forc_version)?;
// write contents to outfile
for doc in rendered_docs.0 {
let mut doc_path = doc_path.clone();
for prefix in doc.module_info.0 {
for prefix in doc.module_info.module_prefixes {
if &prefix != project_name {
doc_path.push(prefix);
}

View file

@ -6,7 +6,7 @@ use std::collections::BTreeMap;
use std::fmt::Write;
use sway_core::language::ty::{
TyDeclaration::{self, *},
TyEnumVariant, TyStorageField, TyStructField, TyTraitFn,
TyEnumVariant, TyProgramKind, TyStorageField, TyStructField, TyTraitFn,
};
use sway_core::transform::{AttributeKind, AttributesMap};
use sway_lsp::utils::markdown::format_docs;
@ -29,14 +29,22 @@ pub(crate) struct RenderedDocumentation(pub(crate) Vec<RenderedDocument>);
impl RenderedDocumentation {
/// Top level HTML rendering for all [Documentation] of a program.
pub fn from(raw: Documentation, forc_version: Option<String>) -> Result<RenderedDocumentation> {
pub fn from(
raw: Documentation,
root_attributes: Option<AttributesMap>,
program_kind: TyProgramKind,
forc_version: Option<String>,
) -> Result<RenderedDocumentation> {
let mut rendered_docs: RenderedDocumentation = Default::default();
let root_module = match raw.first() {
Some(doc) => ModuleInfo::from_vec(vec![doc.module_info.project_name().to_owned()]),
Some(doc) => ModuleInfo::from_ty_module(
vec![doc.module_info.project_name().to_owned()],
root_attributes.map(|attrs_map| attrs_map.to_html_string()),
),
None => panic!("Project does not contain a root module"),
};
let mut all_docs = DocLinks {
style: DocStyle::AllDoc,
style: DocStyle::AllDoc(program_kind.as_title_str().to_string()),
links: Default::default(),
};
let mut module_map: BTreeMap<ModulePrefix, BTreeMap<BlockTitle, Vec<DocLink>>> =
@ -138,7 +146,7 @@ impl RenderedDocumentation {
name: location.clone(),
module_info: doc.module_info.to_owned(),
html_filename: INDEX_FILENAME.to_owned(),
preview_opt: None,
preview_opt: doc.module_info.preview_opt(),
};
match module_map.get_mut(parent_module) {
Some(doc_links) => match doc_links.get_mut(&BlockTitle::Modules) {
@ -227,7 +235,7 @@ impl RenderedDocumentation {
version_opt: forc_version,
module_info: root_module.clone(),
module_docs: DocLinks {
style: DocStyle::ProjectIndex,
style: DocStyle::ProjectIndex(program_kind.as_title_str().to_string()),
links: doc_links.to_owned(),
},
}
@ -485,7 +493,7 @@ impl ItemContext {
.iter()
.map(|field| DocLink {
name: field.name.as_str().to_string(),
module_info: ModuleInfo::from_vec(vec![]),
module_info: ModuleInfo::from_ty_module(vec![], None),
html_filename: format!(
"{}structfield.{}",
IDENTITY,
@ -501,7 +509,7 @@ impl ItemContext {
.iter()
.map(|field| DocLink {
name: field.name.as_str().to_string(),
module_info: ModuleInfo::from_vec(vec![]),
module_info: ModuleInfo::from_ty_module(vec![], None),
html_filename: format!(
"{}storagefield.{}",
IDENTITY,
@ -517,7 +525,7 @@ impl ItemContext {
.iter()
.map(|variant| DocLink {
name: variant.name.as_str().to_string(),
module_info: ModuleInfo::from_vec(vec![]),
module_info: ModuleInfo::from_ty_module(vec![], None),
html_filename: format!("{}variant.{}", IDENTITY, variant.name.as_str()),
preview_opt: None,
})
@ -529,7 +537,7 @@ impl ItemContext {
.iter()
.map(|method| DocLink {
name: method.name.as_str().to_string(),
module_info: ModuleInfo::from_vec(vec![]),
module_info: ModuleInfo::from_ty_module(vec![], None),
html_filename: format!(
"{}structfield.{}",
IDENTITY,
@ -749,7 +757,7 @@ struct DocLinks {
impl Renderable for DocLinks {
fn render(self) -> Result<Box<dyn RenderBox>> {
let doc_links = match self.style {
DocStyle::AllDoc => box_html! {
DocStyle::AllDoc(_) => box_html! {
@ for (title, list_items) in self.links {
@ if !list_items.is_empty() {
h3(id=format!("{}", title.html_title_string())) { : title.as_str(); }
@ -777,7 +785,7 @@ impl Renderable for DocLinks {
}
.into_string()
.unwrap(),
DocStyle::ProjectIndex => box_html! {
DocStyle::ProjectIndex(_) => box_html! {
@ for (title, list_items) in self.links {
@ if !list_items.is_empty() {
h3(id=format!("{}", title.html_title_string())) { : title.as_str(); }
@ -914,7 +922,7 @@ impl SidebarNav for AllDocIndex {
fn sidebar(&self) -> Sidebar {
Sidebar {
version_opt: None,
style: DocStyle::AllDoc,
style: self.all_docs.style.clone(),
module_info: self.project_name.clone(),
href_path: INDEX_FILENAME.to_owned(),
nav: self.all_docs.clone(),
@ -989,7 +997,7 @@ pub(crate) struct ModuleIndex {
impl SidebarNav for ModuleIndex {
fn sidebar(&self) -> Sidebar {
let style = match self.module_info.is_root_module() {
true => DocStyle::ProjectIndex,
true => self.module_docs.style.clone(),
false => DocStyle::ModuleIndex,
};
Sidebar {
@ -1006,8 +1014,8 @@ impl Renderable for ModuleIndex {
let doc_links = self.module_docs.clone().render()?;
let sidebar = self.sidebar().render()?;
let title_prefix = match self.module_docs.style {
DocStyle::ProjectIndex => "Project ",
DocStyle::ModuleIndex => "Module ",
DocStyle::ProjectIndex(ref program_type) => format!("{program_type} "),
DocStyle::ModuleIndex => "Module ".to_string(),
_ => unreachable!("Module Index can only be either a project or module at this time."),
};
@ -1079,6 +1087,16 @@ impl Renderable for ModuleIndex {
}
}
}
@ if self.module_info.attributes.is_some() {
details(class="swaydoc-toggle top-doc", open) {
summary(class="hideme") {
span { : "Expand description" }
}
div(class="docblock") {
: Raw(self.module_info.attributes.unwrap())
}
}
}
: doc_links;
}
}
@ -1094,8 +1112,8 @@ trait SidebarNav {
}
#[derive(Clone, Ord, PartialOrd, Eq, PartialEq)]
enum DocStyle {
AllDoc,
ProjectIndex,
AllDoc(String),
ProjectIndex(String),
ModuleIndex,
Item,
}
@ -1115,8 +1133,8 @@ impl Renderable for Sidebar {
.module_info
.to_html_shorthand_path_string("assets/sway-logo.svg");
let location_with_prefix = match &self.style {
DocStyle::AllDoc | DocStyle::ProjectIndex => {
format!("Project {}", self.module_info.location())
DocStyle::AllDoc(project_kind) | DocStyle::ProjectIndex(project_kind) => {
format!("{project_kind} {}", self.module_info.location())
}
DocStyle::ModuleIndex | DocStyle::Item => format!(
"{} {}",
@ -1125,15 +1143,17 @@ impl Renderable for Sidebar {
),
};
let (logo_path_to_parent, path_to_parent_or_self) = match &self.style {
DocStyle::AllDoc | DocStyle::Item => (self.href_path.clone(), self.href_path.clone()),
DocStyle::ProjectIndex => (IDENTITY.to_owned(), IDENTITY.to_owned()),
DocStyle::AllDoc(_) | DocStyle::Item => {
(self.href_path.clone(), self.href_path.clone())
}
DocStyle::ProjectIndex(_) => (IDENTITY.to_owned(), IDENTITY.to_owned()),
DocStyle::ModuleIndex => (format!("../{INDEX_FILENAME}"), IDENTITY.to_owned()),
};
// Unfortunately, match arms that return a closure, even if they are the same
// type, are incompatible. The work around is to return a String instead,
// and render it from Raw in the final output.
let styled_content = match &self.style {
DocStyle::ProjectIndex => {
DocStyle::ProjectIndex(_) => {
let nav_links = self.nav.links;
let version = match self.version_opt {
Some(ref v) => v.as_str(),

View file

@ -465,6 +465,15 @@ impl TyProgramKind {
TyProgramKind::Script { .. } => parsed::TreeType::Script,
}
}
/// Used for project titles in `forc doc`.
pub fn as_title_str(&self) -> &str {
match self {
TyProgramKind::Contract { .. } => "Contract",
TyProgramKind::Library { .. } => "Library",
TyProgramKind::Predicate { .. } => "Predicate",
TyProgramKind::Script { .. } => "Script",
}
}
}
fn disallow_impure_functions(

View file

@ -1,3 +1,4 @@
//! The official standard library for the Sway smart contract language.
library std;
dep error_signals;

View file

@ -498,7 +498,7 @@ async fn go_to_definition_for_paths() {
req_uri: &uri,
req_line: 10,
req_char: 13,
def_line: 0,
def_line: 1,
def_start_char: 8,
def_end_char: 11,
def_path: "sway-lib-std/src/lib.sw",

View file

@ -25,7 +25,11 @@ pub use crate::{
token::{lex, lex_commented},
};
use sway_ast::{attribute::Annotated, Module, ModuleKind};
use sway_ast::{
attribute::Annotated,
token::{DocComment, DocStyle},
Module, ModuleKind,
};
use sway_error::handler::{ErrorEmitted, Handler};
use std::{path::PathBuf, sync::Arc};
@ -45,5 +49,13 @@ pub fn parse_module_kind(
path: Option<Arc<PathBuf>>,
) -> Result<ModuleKind, ErrorEmitted> {
let ts = lex(handler, &src, 0, src.len(), path)?;
Parser::new(handler, &ts).parse()
let mut parser = Parser::new(handler, &ts);
while let Some(DocComment {
doc_style: DocStyle::Inner,
..
}) = parser.peek()
{
parser.parse::<DocComment>()?;
}
parser.parse()
}