sway/forc-plugins/forc-doc/src/doc/descriptor.rs
Joshua Batty c7ff31207a
Some checks are pending
Codspeed Benchmarks / benchmarks (push) Waiting to run
CI / build-sway-lib-std (push) Waiting to run
CI / get-fuel-core-version (push) Waiting to run
CI / check-forc-manifest-version (push) Waiting to run
CI / verifications-complete (push) Blocked by required conditions
CI / check-dependency-version-formats (push) Waiting to run
CI / build-sway-examples (push) Waiting to run
CI / build-reference-examples (push) Waiting to run
CI / forc-fmt-check-sway-lib-std (push) Waiting to run
CI / forc-fmt-check-sway-examples (push) Waiting to run
CI / forc-fmt-check-panic (push) Waiting to run
CI / check-sdk-harness-test-suite-compatibility (push) Waiting to run
CI / build-mdbook (push) Waiting to run
CI / build-forc-doc-sway-lib-std (push) Waiting to run
CI / build-forc-test-project (push) Waiting to run
CI / cargo-build-workspace (push) Waiting to run
CI / cargo-clippy (push) Waiting to run
CI / cargo-toml-fmt-check (push) Waiting to run
CI / cargo-fmt-check (push) Waiting to run
CI / cargo-test-lib-std (push) Waiting to run
CI / forc-unit-tests (push) Waiting to run
CI / forc-pkg-fuels-deps-check (push) Waiting to run
CI / cargo-test-forc-debug (push) Blocked by required conditions
CI / cargo-test-forc-client (push) Blocked by required conditions
CI / cargo-test-forc-mcp (push) Blocked by required conditions
CI / cargo-test-forc-node (push) Blocked by required conditions
CI / cargo-test-sway-lsp (push) Waiting to run
CI / cargo-test-forc (push) Waiting to run
CI / cargo-test-workspace (push) Waiting to run
CI / cargo-unused-deps-check (push) Waiting to run
CI / notify-slack-on-failure (push) Blocked by required conditions
CI / pre-publish-check (push) Waiting to run
CI / publish (push) Blocked by required conditions
CI / publish-sway-lib-std (push) Blocked by required conditions
CI / Build and upload forc binaries to release (push) Blocked by required conditions
github pages / deploy (push) Waiting to run
CI / cargo-run-e2e-test (push) Blocked by required conditions
CI / cargo-run-e2e-test-release (push) Blocked by required conditions
CI / cargo-run-e2e-test-evm (push) Waiting to run
CI / forc-run-benchmarks (push) Waiting to run
Add parallel processing to forc-doc using rayon to improve performance (#7296)
## Description
Key changes:
  - Parallelize document rendering and link generation
- Add type aliases for complex nested types (`DocLinkMap`, `ModuleMap`,
`RenderResult`)
  - Remove unnecessary wrapper functions and clones

The parallel processing maintains insertion order through sequential
merging, ensuring identical documentation output.

Not sure why the codspeed report isn't showing this below but when I run
`cargo bench` locally i'm seeing these results.

| Metric | Before | After | Improvement |
|--------|--------|-------|-------------|
| build_std_lib_docs | 70.069 ms | 43.227 ms | **43.2% faster** |

## Checklist

- [ ] I have linked to any relevant issues.
- [x] I have commented my code, particularly in hard-to-understand
areas.
- [ ] I have updated the documentation where relevant (API docs, the
reference, and the Sway book).
- [ ] If my change requires substantial documentation changes, I have
[requested support from the DevRel
team](https://github.com/FuelLabs/devrel-requests/issues/new/choose)
- [ ] I have added tests that prove my fix is effective or that my
feature works.
- [ ] 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.
2025-07-30 14:21:46 +04:00

391 lines
18 KiB
Rust

//! Determine whether a [Declaration] is documentable.
use crate::{
doc::{module::ModuleInfo, Document},
render::{
item::{
components::*,
context::{Context, ContextType, ItemContext},
documentable_type::DocumentableType,
},
util::format::docstring::DocStrings,
},
};
use anyhow::Result;
use sway_core::{
decl_engine::*,
language::ty::{self, TyTraitFn, TyTraitInterfaceItem},
Engines, TypeInfo,
};
use sway_features::ExperimentalFeatures;
use sway_types::{integer_bits::IntegerBits, Ident};
use swayfmt::parse;
trait RequiredMethods {
fn to_methods(&self, decl_engine: &DeclEngine) -> Vec<TyTraitFn>;
}
impl RequiredMethods for Vec<DeclRefTraitFn> {
fn to_methods(&self, decl_engine: &DeclEngine) -> Vec<TyTraitFn> {
self.iter()
.map(|decl_ref| decl_engine.get_trait_fn(decl_ref).as_ref().clone())
.collect()
}
}
/// Used in deciding whether or not a [Declaration] is documentable.
pub(crate) enum Descriptor {
Documentable(Document),
NonDocumentable,
}
impl Descriptor {
/// Decides whether a [ty::TyDecl] is [Descriptor::Documentable] and returns a [Document] if so.
pub(crate) fn from_typed_decl(
decl_engine: &DeclEngine,
ty_decl: &ty::TyDecl,
module_info: ModuleInfo,
document_private_items: bool,
experimental: ExperimentalFeatures,
) -> Result<Self> {
const CONTRACT_STORAGE: &str = "Contract Storage";
match ty_decl {
ty::TyDecl::StructDecl(ty::StructDecl { decl_id, .. }) => {
let struct_decl = decl_engine.get_struct(decl_id);
if !document_private_items && struct_decl.visibility.is_private() {
Ok(Descriptor::NonDocumentable)
} else {
let item_name = struct_decl.call_path.suffix.clone();
let attrs_opt = (!struct_decl.attributes.is_empty())
.then(|| struct_decl.attributes.to_html_string());
let context = (!struct_decl.fields.is_empty()).then_some(Context::new(
module_info.clone(),
ContextType::StructFields(struct_decl.fields.clone()),
));
Ok(Descriptor::Documentable(Document {
module_info: module_info.clone(),
item_header: ItemHeader {
module_info: module_info.clone(),
friendly_name: ty_decl.friendly_type_name(),
item_name: item_name.clone(),
},
item_body: ItemBody {
module_info,
ty: DocumentableType::Declared(ty_decl.clone()),
item_name,
code_str: parse::parse_format::<sway_ast::ItemStruct>(
struct_decl.span.as_str(),
experimental,
)?,
attrs_opt: attrs_opt.clone(),
item_context: ItemContext {
context_opt: context,
..Default::default()
},
},
raw_attributes: attrs_opt,
}))
}
}
ty::TyDecl::EnumDecl(ty::EnumDecl { decl_id, .. }) => {
let enum_decl = decl_engine.get_enum(decl_id);
if !document_private_items && enum_decl.visibility.is_private() {
Ok(Descriptor::NonDocumentable)
} else {
let item_name = enum_decl.call_path.suffix.clone();
let attrs_opt = (!enum_decl.attributes.is_empty())
.then(|| enum_decl.attributes.to_html_string());
let context = (!enum_decl.variants.is_empty()).then_some(Context::new(
module_info.clone(),
ContextType::EnumVariants(enum_decl.variants.clone()),
));
Ok(Descriptor::Documentable(Document {
module_info: module_info.clone(),
item_header: ItemHeader {
module_info: module_info.clone(),
friendly_name: ty_decl.friendly_type_name(),
item_name: item_name.clone(),
},
item_body: ItemBody {
module_info,
ty: DocumentableType::Declared(ty_decl.clone()),
item_name,
code_str: parse::parse_format::<sway_ast::ItemEnum>(
enum_decl.span.as_str(),
experimental,
)?,
attrs_opt: attrs_opt.clone(),
item_context: ItemContext {
context_opt: context,
..Default::default()
},
},
raw_attributes: attrs_opt,
}))
}
}
ty::TyDecl::TraitDecl(ty::TraitDecl { decl_id, .. }) => {
let trait_decl = (*decl_engine.get_trait(decl_id)).clone();
if !document_private_items && trait_decl.visibility.is_private() {
Ok(Descriptor::NonDocumentable)
} else {
let item_name = trait_decl.name;
let attrs_opt = (!trait_decl.attributes.is_empty())
.then(|| trait_decl.attributes.to_html_string());
let context =
(!trait_decl.interface_surface.is_empty()).then_some(Context::new(
module_info.clone(),
ContextType::RequiredMethods(
trait_decl
.interface_surface
.into_iter()
.filter_map(|item| match item {
TyTraitInterfaceItem::TraitFn(fn_decl) => Some(fn_decl),
_ => None,
})
.collect::<Vec<_>>()
.to_methods(decl_engine),
),
));
Ok(Descriptor::Documentable(Document {
module_info: module_info.clone(),
item_header: ItemHeader {
module_info: module_info.clone(),
friendly_name: ty_decl.friendly_type_name(),
item_name: item_name.clone(),
},
item_body: ItemBody {
module_info,
ty: DocumentableType::Declared(ty_decl.clone()),
item_name,
code_str: parse::parse_format::<sway_ast::ItemTrait>(
trait_decl.span.as_str(),
experimental,
)?,
attrs_opt: attrs_opt.clone(),
item_context: ItemContext {
context_opt: context,
..Default::default()
},
},
raw_attributes: attrs_opt,
}))
}
}
ty::TyDecl::AbiDecl(ty::AbiDecl { decl_id, .. }) => {
let abi_decl = (*decl_engine.get_abi(decl_id)).clone();
let item_name = abi_decl.name;
let attrs_opt =
(!abi_decl.attributes.is_empty()).then(|| abi_decl.attributes.to_html_string());
let context = (!abi_decl.interface_surface.is_empty()).then_some(Context::new(
module_info.clone(),
ContextType::RequiredMethods(
abi_decl
.interface_surface
.into_iter()
.flat_map(|item| match item {
TyTraitInterfaceItem::TraitFn(fn_decl) => Some(fn_decl),
_ => None,
})
.collect::<Vec<_>>()
.to_methods(decl_engine),
),
));
Ok(Descriptor::Documentable(Document {
module_info: module_info.clone(),
item_header: ItemHeader {
module_info: module_info.clone(),
friendly_name: ty_decl.friendly_type_name(),
item_name: item_name.clone(),
},
item_body: ItemBody {
module_info,
ty: DocumentableType::Declared(ty_decl.clone()),
item_name,
code_str: parse::parse_format::<sway_ast::ItemAbi>(
abi_decl.span.as_str(),
experimental,
)?,
attrs_opt: attrs_opt.clone(),
item_context: ItemContext {
context_opt: context,
..Default::default()
},
},
raw_attributes: attrs_opt,
}))
}
ty::TyDecl::StorageDecl(ty::StorageDecl { decl_id, .. }) => {
let storage_decl = decl_engine.get_storage(decl_id);
let item_name = sway_types::BaseIdent::new_no_trim(
sway_types::span::Span::from_string(CONTRACT_STORAGE.to_string()),
);
let attrs_opt = (!storage_decl.attributes.is_empty())
.then(|| storage_decl.attributes.to_html_string());
let context = (!storage_decl.fields.is_empty()).then_some(Context::new(
module_info.clone(),
ContextType::StorageFields(storage_decl.fields.clone()),
));
Ok(Descriptor::Documentable(Document {
module_info: module_info.clone(),
item_header: ItemHeader {
module_info: module_info.clone(),
friendly_name: ty_decl.friendly_type_name(),
item_name: item_name.clone(),
},
item_body: ItemBody {
module_info,
ty: DocumentableType::Declared(ty_decl.clone()),
item_name,
code_str: parse::parse_format::<sway_ast::ItemStorage>(
storage_decl.span.as_str(),
experimental,
)?,
attrs_opt: attrs_opt.clone(),
item_context: ItemContext {
context_opt: context,
..Default::default()
},
},
raw_attributes: attrs_opt,
}))
}
ty::TyDecl::FunctionDecl(ty::FunctionDecl { decl_id, .. }) => {
let fn_decl = decl_engine.get_function(decl_id);
if !document_private_items && fn_decl.visibility.is_private() {
Ok(Descriptor::NonDocumentable)
} else {
let item_name = fn_decl.name.clone();
let attrs_opt = (!fn_decl.attributes.is_empty())
.then(|| fn_decl.attributes.to_html_string());
Ok(Descriptor::Documentable(Document {
module_info: module_info.clone(),
item_header: ItemHeader {
module_info: module_info.clone(),
friendly_name: ty_decl.friendly_type_name(),
item_name: item_name.clone(),
},
item_body: ItemBody {
module_info,
ty: DocumentableType::Declared(ty_decl.clone()),
item_name,
code_str: trim_fn_body(parse::parse_format::<sway_ast::ItemFn>(
fn_decl.span.as_str(),
experimental,
)?),
attrs_opt: attrs_opt.clone(),
item_context: ItemContext {
context_opt: None,
..Default::default()
},
},
raw_attributes: attrs_opt,
}))
}
}
ty::TyDecl::ConstantDecl(ty::ConstantDecl { decl_id, .. }) => {
let const_decl = decl_engine.get_constant(decl_id);
if !document_private_items && const_decl.visibility.is_private() {
Ok(Descriptor::NonDocumentable)
} else {
let item_name = const_decl.call_path.suffix.clone();
let attrs_opt = (!const_decl.attributes.is_empty())
.then(|| const_decl.attributes.to_html_string());
Ok(Descriptor::Documentable(Document {
module_info: module_info.clone(),
item_header: ItemHeader {
module_info: module_info.clone(),
friendly_name: ty_decl.friendly_type_name(),
item_name: item_name.clone(),
},
item_body: ItemBody {
module_info,
ty: DocumentableType::Declared(ty_decl.clone()),
item_name,
code_str: parse::parse_format::<sway_ast::ItemConst>(
const_decl.span.as_str(),
experimental,
)?,
attrs_opt: attrs_opt.clone(),
item_context: Default::default(),
},
raw_attributes: attrs_opt,
}))
}
}
_ => Ok(Descriptor::NonDocumentable),
}
}
/// Decides whether a [TypeInfo] is [Descriptor::Documentable] and returns a [Document] if so.
pub(crate) fn from_type_info(
type_info: &TypeInfo,
engines: &Engines,
module_info: ModuleInfo,
) -> Result<Self> {
// Only primitive types will result in a documentable item. All other type documentation should come
// from the a declaration. Since primitive types do not have sway declarations, we can only generate
// documentation from their implementations.
let item_name = Ident::new_no_span(format!("{}", engines.help_out(type_info)));
// Build a fake module info for the primitive type.
let module_info = ModuleInfo {
module_prefixes: vec![module_info.project_name().into()],
attributes: None,
};
// TODO: Find a way to add descriptions without hardcoding them.
let description = match type_info {
TypeInfo::StringSlice => "string slice",
TypeInfo::StringArray(_) => "fixed-length string",
TypeInfo::Boolean => "Boolean true or false",
TypeInfo::B256 => "256 bits (32 bytes), i.e. a hash",
TypeInfo::UnsignedInteger(bits) => match bits {
IntegerBits::Eight => "8-bit unsigned integer",
IntegerBits::Sixteen => "16-bit unsigned integer",
IntegerBits::ThirtyTwo => "32-bit unsigned integer",
IntegerBits::SixtyFour => "64-bit unsigned integer",
IntegerBits::V256 => "256-bit unsigned integer",
},
_ => return Ok(Descriptor::NonDocumentable),
};
let attrs_opt = Some(description.to_string());
match type_info {
TypeInfo::StringSlice
| TypeInfo::StringArray(_)
| TypeInfo::Boolean
| TypeInfo::B256
| TypeInfo::UnsignedInteger(_) => Ok(Descriptor::Documentable(Document {
module_info: module_info.clone(),
item_header: ItemHeader {
module_info: module_info.clone(),
friendly_name: "primitive",
item_name: item_name.clone(),
},
item_body: ItemBody {
module_info,
ty: DocumentableType::Primitive(type_info.clone()),
item_name: item_name.clone(),
code_str: item_name.to_string(),
attrs_opt: attrs_opt.clone(),
item_context: Default::default(),
},
raw_attributes: attrs_opt,
})),
_ => Ok(Descriptor::NonDocumentable),
}
}
}
/// Takes a formatted function signature & body and returns only the signature.
fn trim_fn_body(f: String) -> String {
match f.find('{') {
Some(index) => f.split_at(index).0.to_string(),
None => f,
}
}