From 1abcba9afaab9230e71ea207366e65a37803f8c3 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Mon, 24 Nov 2025 17:56:16 +0300 Subject: [PATCH 01/32] add code block logic --- extensions/scarb-doc/src/code_blocks.rs | 183 ++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 extensions/scarb-doc/src/code_blocks.rs diff --git a/extensions/scarb-doc/src/code_blocks.rs b/extensions/scarb-doc/src/code_blocks.rs new file mode 100644 index 000000000..64ee71499 --- /dev/null +++ b/extensions/scarb-doc/src/code_blocks.rs @@ -0,0 +1,183 @@ +use cairo_lang_doc::parser::DocumentationCommentToken; +use std::str::from_utf8; + +/// Represents code block extracted from doc comments. +#[derive(Debug, Clone, PartialEq)] +pub struct DocCodeBlock { + pub code: String, + pub language: String, + pub attributes: Vec, + pub item_full_path: String, + pub close_token_idx: usize, +} + +impl DocCodeBlock { + pub fn new( + code: String, + info_string: &String, + item_full_path: String, + close_token_idx: usize, + ) -> Self { + let (language, attributes) = Self::parse_info_string(info_string); + Self { + code, + language, + attributes, + item_full_path, + close_token_idx, + } + } + + pub fn is_runnable(&self) -> bool { + self.language == "cairo" && + self.attributes.iter().any(|attr| attr == "runnable") + } + + /// Parses info string into language and attributes. The results are lowercased. + fn parse_info_string(info_string: &str) -> (String, Vec) { + let parts: Vec<_> = info_string + .trim() + .split(',') + .map(|s| s.trim().to_ascii_lowercase()) + .filter(|s| !s.is_empty()) + .collect(); + + if parts.is_empty() { + return (String::new(), Vec::new()); + } + let language = parts[0].to_string(); + let attributes = parts[1..].iter().map(|s| s.to_string()).collect(); + (language, attributes) + } +} + +/// Collects code blocks from documentation comment tokens. +pub fn collect_code_blocks( + doc_tokens: &Option>, + full_path: &str, +) -> Vec { + let Some(tokens) = doc_tokens else { + return Vec::new(); + }; + + #[derive(Debug)] + struct CodeFence { + token_idx: usize, + char: u8, + len: usize, + info_string: String, + } + + let mut code_blocks = Vec::new(); + let mut current_fence: Option = None; + + for (idx, token) in tokens.iter().enumerate() { + let content = match token { + DocumentationCommentToken::Content(content) => content.trim(), + DocumentationCommentToken::Link(_) => continue, + }; + if content.is_empty() { + continue; + } + match current_fence { + // Handle potential closing fence. + Some(ref opening) => { + if is_matching_closing_fence(content, opening.char, opening.len) { + let end_idx = idx; + let body = get_block_body(tokens, opening.token_idx + 1, end_idx); + + // Skip empty code blocks. + if !body.trim().is_empty() { + code_blocks.push(DocCodeBlock::new( + body, + &opening.info_string, + full_path.to_string(), + end_idx, + )); + } + current_fence = None; + } + } + // Handle potential opening fence. + None => { + if let Some((len, char)) = scan_code_fence(content.as_bytes()) { + let bytes = content.as_bytes(); + let after = &bytes[len..]; + let info_string = from_utf8(after).unwrap_or("").trim().to_string(); + + current_fence = Some(CodeFence { + token_idx: idx, + char, + len, + info_string, + }); + } + } + } + } + // There may be an unterminated fence at this point, but this is allowed from the spec perspective, so we ignore it. + code_blocks +} + +fn get_block_body( + tokens: &[DocumentationCommentToken], + start_idx: usize, + end_idx: usize, +) -> String { + tokens[start_idx..end_idx] + .iter() + .filter_map(|token| match token { + DocumentationCommentToken::Content(content) => Some(content.as_str()), + DocumentationCommentToken::Link(_) => None, + }) + .collect::>() + .join("") + .trim() + .to_string() +} + +/// Checks if the given `content` is a closing fence matching the given opening fence. +fn is_matching_closing_fence(content: &str, opening_char: u8, opening_len: usize) -> bool { + let bytes = content.as_bytes(); + let Some((len, ch)) = scan_code_fence(bytes) else { + return false; + }; + ch == opening_char + && len >= opening_len + && bytes[len..] + .iter() + .all(|&b| matches!(b, b' ' | b'\t' | b'\r' | b'\n')) +} + +/// Copied from https://github.com/pulldown-cmark/pulldown-cmark/blob/a574ea8a5e6fda7bc26542a612130a2b458a68a7/pulldown-cmark/src/scanners.rs#L744 +fn scan_code_fence(data: &[u8]) -> Option<(usize, u8)> { + let c = *data.first()?; + if !(c == b'`' || c == b'~') { + return None; + } + let i = 1 + scan_ch_repeat(&data[1..], c); + if i >= 3 { + if c == b'`' { + let suffix = &data[i..]; + let next_line = i + scan_nextline(suffix); + // FIXME: make sure this is correct + if suffix[..(next_line - i)].contains(&b'`') { + return None; + } + } + Some((i, c)) + } else { + None + } +} + +fn scan_ch_repeat(data: &[u8], c: u8) -> usize { + data.iter().take_while(|&&b| b == c).count() +} + +fn scan_nextline(bytes: &[u8]) -> usize { + bytes + .iter() + .position(|&b| b == b'\n') + .map_or(bytes.len(), |x| x + 1) +} From b412f49b21ea675b702412aac2430feb39cb2d03 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Mon, 24 Nov 2025 18:37:43 +0300 Subject: [PATCH 02/32] initial integration --- extensions/scarb-doc/src/docs_generation.rs | 6 +++ extensions/scarb-doc/src/lib.rs | 1 + extensions/scarb-doc/src/types/item_data.rs | 42 ++++++++++++++----- extensions/scarb-doc/src/types/other_types.rs | 4 ++ 4 files changed, 42 insertions(+), 11 deletions(-) diff --git a/extensions/scarb-doc/src/docs_generation.rs b/extensions/scarb-doc/src/docs_generation.rs index 467cf5402..9ba44e97f 100644 --- a/extensions/scarb-doc/src/docs_generation.rs +++ b/extensions/scarb-doc/src/docs_generation.rs @@ -1,3 +1,4 @@ +use crate::code_blocks::DocCodeBlock; use crate::location_links::DocLocationLink; use crate::types::module_type::Module; use crate::types::other_types::{ @@ -79,6 +80,7 @@ pub trait DocItem { fn doc_location_links(&self) -> &Vec; fn markdown_formatted_path(&self) -> String; fn group_name(&self) -> &Option; + fn code_blocks(&self) -> &Vec; } macro_rules! impl_doc_item { @@ -113,6 +115,10 @@ macro_rules! impl_doc_item { fn group_name(&self) -> &Option { &self.item_data.group } + + fn code_blocks(&self) -> &Vec { + &self.item_data.code_blocks + } } }; } diff --git a/extensions/scarb-doc/src/lib.rs b/extensions/scarb-doc/src/lib.rs index 208a438b3..45b2b2ac7 100644 --- a/extensions/scarb-doc/src/lib.rs +++ b/extensions/scarb-doc/src/lib.rs @@ -26,6 +26,7 @@ use scarb_ui::Ui; use serde::Serialize; pub mod attributes; +pub mod code_blocks; pub mod db; pub mod diagnostics; pub mod docs_generation; diff --git a/extensions/scarb-doc/src/types/item_data.rs b/extensions/scarb-doc/src/types/item_data.rs index 135902f08..d59456d03 100644 --- a/extensions/scarb-doc/src/types/item_data.rs +++ b/extensions/scarb-doc/src/types/item_data.rs @@ -10,6 +10,7 @@ use cairo_lang_filesystem::ids::CrateId; use serde::Serialize; use serde::Serializer; use std::fmt::Debug; +use crate::code_blocks::{collect_code_blocks, DocCodeBlock}; #[derive(Debug, Serialize, Clone)] pub struct ItemData<'db> { @@ -23,6 +24,8 @@ pub struct ItemData<'db> { pub signature: Option, pub full_path: String, #[serde(skip_serializing)] + pub code_blocks: Vec, + #[serde(skip_serializing)] pub doc_location_links: Vec, pub group: Option, } @@ -41,13 +44,18 @@ impl<'db> ItemData<'db> { .map(|link| DocLocationLink::new(link.start, link.end, link.item_id, db)) .collect::>(); let group = find_groups_from_attributes(db, &id); + let full_path = id.full_path(db); + let doc = db.get_item_documentation_as_tokens(documentable_item_id); + let code_blocks = collect_code_blocks(&doc, &full_path); + Self { id: documentable_item_id, name: id.name(db).to_string(db), - doc: db.get_item_documentation_as_tokens(documentable_item_id), + doc, signature, full_path: format!("{}::{}", parent_full_path, id.name(db).long(db)), parent_full_path: Some(parent_full_path), + code_blocks, doc_location_links, group, } @@ -58,17 +66,22 @@ impl<'db> ItemData<'db> { id: impl TopLevelLanguageElementId<'db>, documentable_item_id: DocumentableItemId<'db>, ) -> Self { + let full_path = format!( + "{}::{}", + doc_full_path(&id.parent_module(db), db), + id.name(db).long(db) + ); + let doc = db.get_item_documentation_as_tokens(documentable_item_id); + let code_blocks = collect_code_blocks(&doc, &full_path); + Self { id: documentable_item_id, name: id.name(db).to_string(db), - doc: db.get_item_documentation_as_tokens(documentable_item_id), + doc, signature: None, - full_path: format!( - "{}::{}", - doc_full_path(&id.parent_module(db), db), - id.name(db).long(db) - ), - parent_full_path: Some(doc_full_path(&id.parent_module(db), db)), + full_path, + parent_full_path: Some(id.parent_module(db).full_path(db)), + code_blocks, doc_location_links: vec![], group: find_groups_from_attributes(db, &id), } @@ -76,13 +89,18 @@ impl<'db> ItemData<'db> { pub fn new_crate(db: &'db ScarbDocDatabase, id: CrateId<'db>) -> Self { let documentable_id = DocumentableItemId::Crate(id); + let full_path = ModuleId::CrateRoot(id).full_path(db); + let doc = db.get_item_documentation_as_tokens(documentable_id); + let code_blocks = collect_code_blocks(&doc, &full_path); + Self { id: documentable_id, name: id.long(db).name().to_string(db), - doc: db.get_item_documentation_as_tokens(documentable_id), + doc, signature: None, - full_path: ModuleId::CrateRoot(id).full_path(db), + full_path, parent_full_path: None, + code_blocks, doc_location_links: vec![], group: None, } @@ -116,6 +134,8 @@ impl<'db> From> for ItemData<'db> { doc: val.doc, signature: val.signature, full_path: val.full_path, + // TODO: fix this + code_blocks: Default::default(), doc_location_links: val.doc_location_links, group: val.group, } @@ -140,7 +160,7 @@ impl<'db> From> for SubItemData<'db> { fn documentation_serializer( docs: &Option>, serializer: S, -) -> anyhow::Result +) -> Result where S: Serializer, { diff --git a/extensions/scarb-doc/src/types/other_types.rs b/extensions/scarb-doc/src/types/other_types.rs index d36522746..41b535cc9 100644 --- a/extensions/scarb-doc/src/types/other_types.rs +++ b/extensions/scarb-doc/src/types/other_types.rs @@ -1,3 +1,7 @@ +use anyhow::Result; + +use crate::attributes::find_groups_from_attributes; +use crate::code_blocks::{DocCodeBlock, collect_code_blocks}; use crate::db::ScarbDocDatabase; use crate::docs_generation::markdown::context::IncludedItems; use crate::docs_generation::markdown::traits::WithPath; From fbb921df2c25c3b077742078066d399c147d3f1b Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Tue, 25 Nov 2025 04:35:46 +0300 Subject: [PATCH 03/32] initial support for runnable snippets in `scarb doc` --- extensions/scarb-doc/Cargo.toml | 3 + extensions/scarb-doc/src/code_blocks.rs | 43 ++-- .../scarb-doc/src/docs_generation/markdown.rs | 7 +- .../src/docs_generation/markdown/summary.rs | 20 +- .../docs_generation/markdown/summary/files.rs | 107 ++++++-- .../markdown/summary/group_files.rs | 23 +- .../src/docs_generation/markdown/traits.rs | 123 +++++++-- extensions/scarb-doc/src/lib.rs | 5 + extensions/scarb-doc/src/main.rs | 19 +- extensions/scarb-doc/src/runner.rs | 238 ++++++++++++++++++ extensions/scarb-doc/src/types/module_type.rs | 47 ++++ 11 files changed, 564 insertions(+), 71 deletions(-) create mode 100644 extensions/scarb-doc/src/runner.rs diff --git a/extensions/scarb-doc/Cargo.toml b/extensions/scarb-doc/Cargo.toml index 804eb2766..c166dc22f 100644 --- a/extensions/scarb-doc/Cargo.toml +++ b/extensions/scarb-doc/Cargo.toml @@ -21,11 +21,14 @@ cairo-lang-semantic.workspace = true cairo-lang-starknet.workspace = true cairo-lang-syntax.workspace = true cairo-lang-utils.workspace = true +create-output-dir = { path = "../../utils/create-output-dir" } expect-test.workspace = true indoc.workspace = true itertools.workspace = true mimalloc.workspace = true +tempfile.workspace = true scarb-metadata = { path = "../../scarb-metadata" } +scarb-build-metadata = { path = "../../utils/scarb-build-metadata" } scarb-ui = { path = "../../utils/scarb-ui" } scarb-extensions-cli = { path = "../../utils/scarb-extensions-cli", default-features = false, features = ["doc"] } serde.workspace = true diff --git a/extensions/scarb-doc/src/code_blocks.rs b/extensions/scarb-doc/src/code_blocks.rs index 64ee71499..7ad0376e6 100644 --- a/extensions/scarb-doc/src/code_blocks.rs +++ b/extensions/scarb-doc/src/code_blocks.rs @@ -1,36 +1,43 @@ use cairo_lang_doc::parser::DocumentationCommentToken; use std::str::from_utf8; +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct DocCodeBlockId { + pub item_full_path: String, + pub close_token_idx: usize, +} + +impl DocCodeBlockId { + pub fn new(item_full_path: String, close_token_idx: usize) -> Self { + Self { + item_full_path, + close_token_idx, + } + } +} + /// Represents code block extracted from doc comments. #[derive(Debug, Clone, PartialEq)] pub struct DocCodeBlock { + pub id: DocCodeBlockId, pub code: String, pub language: String, pub attributes: Vec, - pub item_full_path: String, - pub close_token_idx: usize, } impl DocCodeBlock { - pub fn new( - code: String, - info_string: &String, - item_full_path: String, - close_token_idx: usize, - ) -> Self { + pub fn new(id: DocCodeBlockId, code: String, info_string: &String) -> Self { let (language, attributes) = Self::parse_info_string(info_string); Self { + id, code, language, attributes, - item_full_path, - close_token_idx, } } pub fn is_runnable(&self) -> bool { - self.language == "cairo" && - self.attributes.iter().any(|attr| attr == "runnable") + self.language == "cairo" && self.attributes.iter().any(|attr| attr == "runnable") } /// Parses info string into language and attributes. The results are lowercased. @@ -84,15 +91,17 @@ pub fn collect_code_blocks( Some(ref opening) => { if is_matching_closing_fence(content, opening.char, opening.len) { let end_idx = idx; - let body = get_block_body(tokens, opening.token_idx + 1, end_idx); + let body = get_block_body(tokens, opening.token_idx + 1, end_idx) + .trim() + .to_string(); // Skip empty code blocks. - if !body.trim().is_empty() { + let id = DocCodeBlockId::new(full_path.to_string(), idx); + if !body.is_empty() { code_blocks.push(DocCodeBlock::new( - body, + id, + body.to_string(), &opening.info_string, - full_path.to_string(), - end_idx, )); } current_fence = None; diff --git a/extensions/scarb-doc/src/docs_generation/markdown.rs b/extensions/scarb-doc/src/docs_generation/markdown.rs index 5ac2f722e..cbe943583 100644 --- a/extensions/scarb-doc/src/docs_generation/markdown.rs +++ b/extensions/scarb-doc/src/docs_generation/markdown.rs @@ -14,6 +14,7 @@ pub mod traits; use crate::docs_generation::common::{ GeneratedFile, OutputFilesExtension, SummaryIndexMap, SummaryListItem, }; +use crate::runner::CodeBlockExecutionResult; use std::ops::Add; const BASE_HEADER_LEVEL: usize = 1; @@ -40,10 +41,10 @@ impl MarkdownContent { pub fn from_crate( package_information: &PackageInformation, format: OutputFilesExtension, + execution_results: Option>, ) -> Result { let (summary, doc_files) = - generate_summary_file_content(&package_information.crate_, format)?; - + generate_summary_file_content(&package_information.crate_, format, execution_results)?; Ok(Self { book_toml: generate_book_toml_content(&package_information.metadata), summary, @@ -77,7 +78,7 @@ impl WorkspaceMarkdownBuilder { self.book_toml = Some(generate_book_toml_content(&package_information.metadata)); } let (summary, files) = - generate_summary_file_content(&package_information.crate_, self.output_format)?; + generate_summary_file_content(&package_information.crate_, self.output_format, None)?; let current = std::mem::replace(&mut self.summary, SummaryIndexMap::new()); self.summary = current.add(summary); self.doc_files.extend(files); diff --git a/extensions/scarb-doc/src/docs_generation/markdown/summary.rs b/extensions/scarb-doc/src/docs_generation/markdown/summary.rs index 67dd3c9b4..33a187ed6 100644 --- a/extensions/scarb-doc/src/docs_generation/markdown/summary.rs +++ b/extensions/scarb-doc/src/docs_generation/markdown/summary.rs @@ -13,6 +13,7 @@ use crate::docs_generation::markdown::summary::files::{ }; use crate::docs_generation::markdown::traits::{MarkdownDocItem, TopLevelMarkdownDocItem}; use crate::docs_generation::markdown::{BASE_HEADER_LEVEL, SummaryIndexMap}; +use crate::runner::CodeBlockExecutionResult; use crate::types::crate_type::Crate; use anyhow::Result; use group_files::generate_global_groups_summary_files; @@ -20,6 +21,7 @@ use group_files::generate_global_groups_summary_files; pub fn generate_summary_file_content( crate_: &Crate, output_format: OutputFilesExtension, + execution_results: Option>, ) -> Result<(SummaryIndexMap, Vec<(String, String)>)> { let mut summary_index_map = SummaryIndexMap::new(); let context = MarkdownGenerationContext::from_crate(crate_, output_format); @@ -48,23 +50,33 @@ pub fn generate_summary_file_content( BASE_HEADER_LEVEL, None, &summary_index_map, + execution_results.clone(), )?, )]; - let module_item_summaries = - &generate_modules_summary_files(&crate_.root_module, &context, &summary_index_map)?; + let module_item_summaries = &generate_modules_summary_files( + &crate_.root_module, + &context, + &summary_index_map, + execution_results.clone(), + )?; summary_files.extend(module_item_summaries.to_owned()); let foreign_modules_files = generate_foreign_crates_summary_files( &crate_.foreign_crates, &context, &summary_index_map, + execution_results.clone(), )?; summary_files.extend(foreign_modules_files); - let groups_files = - generate_global_groups_summary_files(&crate_.groups, &context, &summary_index_map)?; + let groups_files = generate_global_groups_summary_files( + &crate_.groups, + &context, + &summary_index_map, + execution_results.clone(), + )?; summary_files.extend(groups_files.to_owned()); Ok((summary_index_map, summary_files)) } diff --git a/extensions/scarb-doc/src/docs_generation/markdown/summary/files.rs b/extensions/scarb-doc/src/docs_generation/markdown/summary/files.rs index 44c3b7286..05ebdd734 100644 --- a/extensions/scarb-doc/src/docs_generation/markdown/summary/files.rs +++ b/extensions/scarb-doc/src/docs_generation/markdown/summary/files.rs @@ -4,10 +4,9 @@ use crate::docs_generation::markdown::traits::{ MarkdownDocItem, TopLevelMarkdownDocItem, generate_markdown_table_summary_for_top_level_subitems, }; -use crate::docs_generation::markdown::{ - BASE_HEADER_LEVEL, BASE_MODULE_CHAPTER_PREFIX, SummaryIndexMap, -}; -use crate::docs_generation::{DocItem, TopLevelItems}; +use crate::docs_generation::markdown::{BASE_HEADER_LEVEL, BASE_MODULE_CHAPTER_PREFIX}; +use crate::docs_generation::{DocItem, TopLevelItems, common}; +use crate::runner::CodeBlockExecutionResult; use crate::types::module_type::Module; use crate::types::other_types::{ Constant, Enum, ExternFunction, ExternType, FreeFunction, Impl, ImplAlias, MacroDeclaration, @@ -15,6 +14,7 @@ use crate::types::other_types::{ }; use crate::types::struct_types::Struct; use anyhow::Result; +use common::SummaryIndexMap; use itertools::chain; macro_rules! module_summary { @@ -39,6 +39,7 @@ pub fn generate_modules_summary_files( module: &Module, context: &MarkdownGenerationContext, summary_index_map: &SummaryIndexMap, + execution_results: Option>, ) -> Result> { let mut top_level_items = TopLevelItems::default(); let Module { @@ -79,14 +80,23 @@ pub fn generate_modules_summary_files( )?; doc_files.extend::>( - generate_doc_files_for_module_items(&top_level_items, context, summary_index_map)? - .to_owned(), + generate_doc_files_for_module_items( + &top_level_items, + context, + summary_index_map, + execution_results.clone(), + )? + .to_owned(), ); if !top_level_items.modules.is_empty() { for submodule in module.submodules.iter() { - let sub_summaries = - &generate_modules_summary_files(submodule, context, summary_index_map)?; + let sub_summaries = &generate_modules_summary_files( + submodule, + context, + summary_index_map, + execution_results.clone(), + )?; doc_files.extend::>(sub_summaries.to_owned()); } } @@ -97,16 +107,27 @@ pub fn generate_foreign_crates_summary_files( foreign_modules: &Vec, context: &MarkdownGenerationContext, summary_index_map: &SummaryIndexMap, + execution_results: Option>, ) -> Result> { let mut summary_files = vec![]; for module in foreign_modules { summary_files.extend(vec![( module.filename(context.files_extension), - module.generate_markdown(context, BASE_HEADER_LEVEL, None, summary_index_map)?, + module.generate_markdown( + context, + BASE_HEADER_LEVEL, + None, + summary_index_map, + execution_results.clone(), + )?, )]); - let module_item_summaries = - &generate_modules_summary_files(module, context, summary_index_map)?; + let module_item_summaries = &generate_modules_summary_files( + module, + context, + summary_index_map, + execution_results.clone(), + )?; summary_files.extend(module_item_summaries.to_owned()); } Ok(summary_files) @@ -146,43 +167,80 @@ pub fn generate_doc_files_for_module_items( top_level_items: &TopLevelItems, context: &MarkdownGenerationContext, summary_index_map: &SummaryIndexMap, + execution_results: Option>, ) -> Result> { Ok(chain!( - generate_top_level_docs_contents(&top_level_items.modules, context, summary_index_map)?, - generate_top_level_docs_contents(&top_level_items.constants, context, summary_index_map)?, + generate_top_level_docs_contents( + &top_level_items.modules, + context, + summary_index_map, + execution_results.clone() + )?, + generate_top_level_docs_contents( + &top_level_items.constants, + context, + summary_index_map, + execution_results.clone() + )?, generate_top_level_docs_contents( &top_level_items.free_functions, context, - summary_index_map + summary_index_map, + execution_results.clone() + )?, + generate_top_level_docs_contents( + &top_level_items.structs, + context, + summary_index_map, + execution_results.clone() + )?, + generate_top_level_docs_contents( + &top_level_items.enums, + context, + summary_index_map, + execution_results.clone() )?, - generate_top_level_docs_contents(&top_level_items.structs, context, summary_index_map)?, - generate_top_level_docs_contents(&top_level_items.enums, context, summary_index_map)?, generate_top_level_docs_contents( &top_level_items.type_aliases, context, - summary_index_map + summary_index_map, + execution_results.clone() )?, generate_top_level_docs_contents( &top_level_items.impl_aliases, context, summary_index_map, + execution_results.clone() + )?, + generate_top_level_docs_contents( + &top_level_items.traits, + context, + summary_index_map, + execution_results.clone() + )?, + generate_top_level_docs_contents( + &top_level_items.impls, + context, + summary_index_map, + execution_results.clone() )?, - generate_top_level_docs_contents(&top_level_items.traits, context, summary_index_map,)?, - generate_top_level_docs_contents(&top_level_items.impls, context, summary_index_map,)?, generate_top_level_docs_contents( &top_level_items.extern_types, context, summary_index_map, + execution_results.clone() )?, generate_top_level_docs_contents( &top_level_items.extern_functions, context, summary_index_map, + execution_results.clone() )?, generate_top_level_docs_contents( &top_level_items.macro_declarations, context, summary_index_map, + execution_results.clone() )?, ) .collect::>()) @@ -192,12 +250,19 @@ fn generate_top_level_docs_contents( items: &[&impl TopLevelMarkdownDocItem], context: &MarkdownGenerationContext, summary_index_map: &SummaryIndexMap, + execution_results: Option>, ) -> Result> { items .iter() .map(|item| { - item.generate_markdown(context, BASE_HEADER_LEVEL, None, summary_index_map) - .map(|markdown| (item.filename(context.files_extension), markdown)) + item.generate_markdown( + context, + BASE_HEADER_LEVEL, + None, + summary_index_map, + execution_results.clone(), + ) + .map(|markdown| (item.filename(context.files_extension), markdown)) }) .collect() } diff --git a/extensions/scarb-doc/src/docs_generation/markdown/summary/group_files.rs b/extensions/scarb-doc/src/docs_generation/markdown/summary/group_files.rs index 59591a880..f99ffcb38 100644 --- a/extensions/scarb-doc/src/docs_generation/markdown/summary/group_files.rs +++ b/extensions/scarb-doc/src/docs_generation/markdown/summary/group_files.rs @@ -1,11 +1,14 @@ use crate::docs_generation::TopLevelItems; +use crate::docs_generation::markdown::GROUP_CHAPTER_PREFIX; use crate::docs_generation::markdown::context::MarkdownGenerationContext; use crate::docs_generation::markdown::summary::files::{ generate_doc_files_for_module_items, generate_modules_summary_files, generate_summary_files_for_module_items, }; use crate::docs_generation::markdown::traits::generate_markdown_table_summary_for_top_level_subitems; -use crate::docs_generation::markdown::{GROUP_CHAPTER_PREFIX, SummaryIndexMap}; + +use crate::docs_generation::common::SummaryIndexMap; +use crate::runner::CodeBlockExecutionResult; use crate::types::groups::Group; use itertools::Itertools; @@ -26,6 +29,7 @@ pub fn generate_global_groups_summary_files( groups: &[Group], context: &MarkdownGenerationContext, summary_index_map: &SummaryIndexMap, + execution_results: Option>, ) -> anyhow::Result> { let mut doc_files: Vec<(String, String)> = Vec::new(); @@ -66,8 +70,13 @@ pub fn generate_global_groups_summary_files( )?); doc_files.extend( - generate_doc_files_for_module_items(&top_level_items, context, summary_index_map)? - .to_owned(), + generate_doc_files_for_module_items( + &top_level_items, + context, + summary_index_map, + execution_results.clone(), + )? + .to_owned(), ); doc_files.push(( @@ -77,8 +86,12 @@ pub fn generate_global_groups_summary_files( if !top_level_items.modules.is_empty() { for submodule in group.submodules.iter() { - let sub_summaries = - &generate_modules_summary_files(submodule, context, summary_index_map)?; + let sub_summaries = &generate_modules_summary_files( + submodule, + context, + summary_index_map, + execution_results.clone(), + )?; doc_files.extend::>(sub_summaries.to_owned()); } }; diff --git a/extensions/scarb-doc/src/docs_generation/markdown/traits.rs b/extensions/scarb-doc/src/docs_generation/markdown/traits.rs index aa9d9cf19..9620fe144 100644 --- a/extensions/scarb-doc/src/docs_generation/markdown/traits.rs +++ b/extensions/scarb-doc/src/docs_generation/markdown/traits.rs @@ -1,9 +1,10 @@ use super::context::MarkdownGenerationContext; use crate::docs_generation::markdown::{ BASE_MODULE_CHAPTER_PREFIX, GROUP_CHAPTER_PREFIX, SHORT_DOCUMENTATION_AVOID_PREFIXES, - SHORT_DOCUMENTATION_LEN, SummaryIndexMap, + SHORT_DOCUMENTATION_LEN, }; -use crate::docs_generation::{DocItem, PrimitiveDocItem, SubPathDocItem, TopLevelDocItem}; +use crate::docs_generation::{DocItem, PrimitiveDocItem, SubPathDocItem, TopLevelDocItem, common}; +use crate::runner::CodeBlockExecutionResult; use crate::types::groups::Group; use crate::types::item_data::{ItemData, SubItemData}; use crate::types::module_type::{Module, ModulePubUses}; @@ -15,6 +16,7 @@ use crate::types::other_types::{ use crate::types::struct_types::{Member, Struct}; use anyhow::Result; use cairo_lang_doc::parser::{CommentLinkToken, DocumentationCommentToken}; +use common::SummaryIndexMap; use itertools::Itertools; use std::collections::HashMap; use std::fmt::Write; @@ -82,6 +84,7 @@ macro_rules! impl_markdown_doc_item { header_level: usize, item_suffix: Option, summary_index_map: &SummaryIndexMap, + execution_results: Option>, ) -> Result { let mut markdown = String::new(); @@ -89,7 +92,7 @@ macro_rules! impl_markdown_doc_item { context.get_header_primitive(header_level, self.name(), self.full_path()); writeln!(&mut markdown, "{}\n", header)?; - if let Some(doc) = self.get_documentation(context) { + if let Some(doc) = self.get_documentation(context, execution_results) { writeln!(&mut markdown, "{doc}\n")?; } @@ -135,6 +138,7 @@ pub trait MarkdownDocItem: DocItem { header_level: usize, item_suffix: Option, summary_index_map: &SummaryIndexMap, + execution_results: Option>, ) -> Result; fn get_short_documentation(&self, context: &MarkdownGenerationContext) -> String { @@ -180,14 +184,34 @@ pub trait MarkdownDocItem: DocItem { "—".to_string() } - fn get_documentation(&self, context: &MarkdownGenerationContext) -> Option { + fn get_documentation( + &self, + context: &MarkdownGenerationContext, + execution_results: Option>, + ) -> Option { self.doc().as_ref().map(|doc_tokens| { + // TODO: filter out execution results that do not belong to this item doc_tokens .iter() - .map(|doc_token| match doc_token { - DocumentationCommentToken::Content(content) => content.clone(), + .enumerate() + .flat_map(|(idx, doc_token)| match doc_token { + DocumentationCommentToken::Content(content) => { + // Check if this token is the closing fence of a code block that has execution results + if let Some(cb) = self + .code_blocks() + .iter() + .find(|cb| cb.id.close_token_idx == idx) + && let Some(results) = &execution_results + && let Some(res) = results + .iter() + .find(|exec_res| exec_res.code_block_id == cb.id) + { + return vec![content.clone(), res.format_as_markdown()]; + } + vec![content.clone()] + } DocumentationCommentToken::Link(link) => { - self.format_link_to_path(link, context) + vec![self.format_link_to_path(link, context)] } }) .join("") @@ -221,8 +245,16 @@ where header_level: usize, _item_suffix: Option, summary_index_map: &SummaryIndexMap, + execution_results: Option>, ) -> Result { - generate_markdown_from_item_data(self, context, header_level, None, summary_index_map) + generate_markdown_from_item_data( + self, + context, + header_level, + None, + summary_index_map, + execution_results, + ) } } @@ -233,9 +265,16 @@ impl<'db> MarkdownDocItem for Enum<'db> { header_level: usize, _item_suffix: Option, summary_index_map: &SummaryIndexMap, + execution_results: Option>, ) -> Result { - let mut markdown = - generate_markdown_from_item_data(self, context, header_level, None, summary_index_map)?; + let mut markdown = generate_markdown_from_item_data( + self, + context, + header_level, + None, + summary_index_map, + execution_results.clone(), + )?; let mut suffix_calculator = ItemSuffixCalculator::new(self.name()); markdown += &generate_markdown_for_subitems( &self.variants, @@ -243,6 +282,7 @@ impl<'db> MarkdownDocItem for Enum<'db> { header_level, &mut suffix_calculator, summary_index_map, + execution_results.clone(), )?; Ok(markdown) @@ -256,9 +296,16 @@ impl<'db> MarkdownDocItem for Impl<'db> { header_level: usize, _item_suffix: Option, summary_index_map: &SummaryIndexMap, + execution_results: Option>, ) -> Result { - let mut markdown = - generate_markdown_from_item_data(self, context, header_level, None, summary_index_map)?; + let mut markdown = generate_markdown_from_item_data( + self, + context, + header_level, + None, + summary_index_map, + execution_results.clone(), + )?; let mut suffix_calculator = ItemSuffixCalculator::new(self.name()); markdown += &generate_markdown_for_subitems( @@ -267,6 +314,7 @@ impl<'db> MarkdownDocItem for Impl<'db> { header_level, &mut suffix_calculator, summary_index_map, + execution_results.clone(), )?; markdown += &generate_markdown_for_subitems( @@ -275,6 +323,7 @@ impl<'db> MarkdownDocItem for Impl<'db> { header_level, &mut suffix_calculator, summary_index_map, + execution_results.clone(), )?; markdown += &generate_markdown_for_subitems( @@ -283,6 +332,7 @@ impl<'db> MarkdownDocItem for Impl<'db> { header_level, &mut suffix_calculator, summary_index_map, + execution_results.clone(), )?; Ok(markdown) @@ -380,9 +430,16 @@ impl<'db> MarkdownDocItem for Module<'db> { header_level: usize, _item_suffix: Option, summary_index_map: &SummaryIndexMap, + execution_results: Option>, ) -> Result { - let mut markdown = - generate_markdown_from_item_data(self, context, header_level, None, summary_index_map)?; + let mut markdown = generate_markdown_from_item_data( + self, + context, + header_level, + None, + summary_index_map, + execution_results, + )?; markdown += &generate_markdown_table_summary_for_top_level_subitems( &self.submodules.iter().collect_vec(), @@ -476,9 +533,16 @@ impl<'db> MarkdownDocItem for Struct<'db> { header_level: usize, _item_suffix: Option, summary_index_map: &SummaryIndexMap, + execution_results: Option>, ) -> Result { - let mut markdown = - generate_markdown_from_item_data(self, context, header_level, None, summary_index_map)?; + let mut markdown = generate_markdown_from_item_data( + self, + context, + header_level, + None, + summary_index_map, + execution_results.clone(), + )?; let mut suffix_calculator = ItemSuffixCalculator::new(self.name()); markdown += &generate_markdown_for_subitems( @@ -487,6 +551,7 @@ impl<'db> MarkdownDocItem for Struct<'db> { header_level, &mut suffix_calculator, summary_index_map, + execution_results.clone(), )?; Ok(markdown) @@ -500,9 +565,16 @@ impl<'db> MarkdownDocItem for Trait<'db> { header_level: usize, _item_suffix: Option, summary_index_map: &SummaryIndexMap, + execution_results: Option>, ) -> Result { - let mut markdown = - generate_markdown_from_item_data(self, context, header_level, None, summary_index_map)?; + let mut markdown = generate_markdown_from_item_data( + self, + context, + header_level, + None, + summary_index_map, + execution_results.clone(), + )?; let mut suffix_calculator = ItemSuffixCalculator::new(self.name()); markdown += &generate_markdown_for_subitems( @@ -511,6 +583,7 @@ impl<'db> MarkdownDocItem for Trait<'db> { header_level, &mut suffix_calculator, summary_index_map, + execution_results.clone(), )?; markdown += &generate_markdown_for_subitems( &self.trait_functions, @@ -518,6 +591,7 @@ impl<'db> MarkdownDocItem for Trait<'db> { header_level, &mut suffix_calculator, summary_index_map, + execution_results.clone(), )?; markdown += &generate_markdown_for_subitems( &self.trait_types, @@ -525,6 +599,7 @@ impl<'db> MarkdownDocItem for Trait<'db> { header_level, &mut suffix_calculator, summary_index_map, + execution_results.clone(), )?; Ok(markdown) } @@ -766,6 +841,7 @@ fn generate_markdown_for_subitems( header_level: usize, suffix_calculator: &mut ItemSuffixCalculator, summary_index_map: &SummaryIndexMap, + execution_results: Option>, ) -> Result { let mut markdown = String::new(); @@ -779,7 +855,13 @@ fn generate_markdown_for_subitems( writeln!( &mut markdown, "{}", - item.generate_markdown(context, header_level + 2, postfix, summary_index_map)? + item.generate_markdown( + context, + header_level + 2, + postfix, + summary_index_map, + execution_results.clone() + )? )?; } } @@ -793,13 +875,14 @@ fn generate_markdown_from_item_data( header_level: usize, item_suffix: Option, summary_index_map: &SummaryIndexMap, + execution_results: Option>, ) -> Result { let mut markdown = String::new(); let header = context.get_header(header_level, doc_item.name(), doc_item.full_path()); writeln!(&mut markdown, "{}\n", header)?; - if let Some(doc) = doc_item.get_documentation(context) { + if let Some(doc) = doc_item.get_documentation(context, execution_results) { writeln!(&mut markdown, "{doc}\n")?; } diff --git a/extensions/scarb-doc/src/lib.rs b/extensions/scarb-doc/src/lib.rs index 45b2b2ac7..ea803ac5c 100644 --- a/extensions/scarb-doc/src/lib.rs +++ b/extensions/scarb-doc/src/lib.rs @@ -33,6 +33,7 @@ pub mod docs_generation; pub mod errors; pub mod location_links; pub mod metadata; +pub mod runner; pub mod types; pub mod versioned_json_output; @@ -40,6 +41,7 @@ pub mod versioned_json_output; pub struct PackageInformation<'db> { pub crate_: Crate<'db>, pub metadata: AdditionalMetadata, + pub package_metadata: PackageMetadata, } #[derive(Serialize, Clone)] @@ -52,6 +54,7 @@ pub struct PackageContext { pub db: ScarbDocDatabase, pub should_document_private_items: bool, pub metadata: AdditionalMetadata, + pub package_metadata: PackageMetadata, package_compilation_unit: Option, main_component: CompilationUnitComponentMetadata, } @@ -99,6 +102,7 @@ pub fn generate_package_context( should_document_private_items, package_compilation_unit, main_component: main_component.clone(), + package_metadata: package_metadata.clone(), metadata: AdditionalMetadata { name: package_metadata.name.clone(), authors, @@ -142,6 +146,7 @@ pub fn generate_package_information( Ok(PackageInformation { crate_, metadata: context.metadata.clone(), + package_metadata: context.package_metadata.clone(), }) } diff --git a/extensions/scarb-doc/src/main.rs b/extensions/scarb-doc/src/main.rs index 96e68332d..ec4922b62 100644 --- a/extensions/scarb-doc/src/main.rs +++ b/extensions/scarb-doc/src/main.rs @@ -7,6 +7,7 @@ use scarb_doc::docs_generation::common::OutputFilesExtension; use scarb_doc::docs_generation::markdown::{MarkdownContent, WorkspaceMarkdownBuilder}; use scarb_doc::errors::{MetadataCommandError, PackagesSerializationError}; use scarb_doc::metadata::get_target_dir; +use scarb_doc::runner::{SnippetRunner, collect_runnable_code_blocks}; use scarb_doc::versioned_json_output::VersionedJsonOutput; use scarb_doc::{PackageInformation, generate_package_context, generate_package_information}; use scarb_extensions_cli::doc::{Args, OutputFormat}; @@ -134,7 +135,23 @@ impl OutputEmit { ui, files_extension, } => { - let content = MarkdownContent::from_crate(&package, *files_extension)?; + // TODO: refactor this + let snippet_execution_enabled = *build; + let execution_results = if snippet_execution_enabled { + let snippets = collect_runnable_code_blocks(&package.crate_); + if !snippets.is_empty() { + let executor = SnippetRunner::new(&package.package_metadata, ui.clone()); + let execution_results = executor.execute(&snippets)?; + Some(execution_results) + } else { + Some(vec![]) + } + } else { + None + }; + + let content = + MarkdownContent::from_crate(&package, *files_extension, execution_results)?; output_markdown( content, Some(package.metadata.name), diff --git a/extensions/scarb-doc/src/runner.rs b/extensions/scarb-doc/src/runner.rs new file mode 100644 index 000000000..baa316447 --- /dev/null +++ b/extensions/scarb-doc/src/runner.rs @@ -0,0 +1,238 @@ +use crate::code_blocks::DocCodeBlockId; +use crate::types::crate_type::Crate; +use crate::types::module_type::Module; +use crate::types::other_types::ItemData; +use anyhow::{Context, Result, anyhow}; +use camino::{Utf8Path, Utf8PathBuf}; +use create_output_dir::{ + EXECUTE_PRINT_OUTPUT_FILENAME, EXECUTE_PROGRAM_OUTPUT_FILENAME, create_output_dir, + incremental_create_execution_output_dir, +}; +use indoc::formatdoc; +use scarb_build_metadata::CAIRO_VERSION; +use scarb_metadata::{PackageMetadata, ScarbCommand}; +use scarb_ui::Ui; +use std::fs; +use tempfile::tempdir; + +#[derive(Debug, Clone)] +pub struct RunnableCodeBlock { + pub code_block_id: DocCodeBlockId, + pub code: String, +} + +#[derive(Debug, Clone)] +pub struct CodeBlockExecutionResult { + pub code_block_id: DocCodeBlockId, + pub print_output: String, + pub program_output: String, +} + +impl CodeBlockExecutionResult { + /// Formats the execution result as markdown with code blocks. + pub fn format_as_markdown(&self) -> String { + let mut output = String::new(); + if !self.print_output.is_empty() { + output.push_str("\nOutput:\n```\n"); + output.push_str(&self.print_output); + output.push_str("\n```\n"); + } + if !self.program_output.is_empty() { + output.push_str("\nResult:\n```\n"); + output.push_str(&self.program_output); + output.push_str("\n```\n"); + } + if output.is_empty() { + output.push_str("\n*No output.*\n"); + } + output + } +} + +/// A runner for executing runnable code runnable_code_blocks extracted from documentation. +/// Uses `scarb execute` to run the runnable_code_blocks in isolated temporary workspaces. +pub struct SnippetRunner<'a> { + package_metadata: &'a PackageMetadata, + ui: Ui, +} + +impl<'a> SnippetRunner<'a> { + pub fn new(package_metadata: &'a PackageMetadata, ui: Ui) -> Self { + Self { + package_metadata, + ui, + } + } + + pub fn execute(&self, snippets: &[RunnableCodeBlock]) -> Result> { + let mut results = Vec::new(); + for (index, snippet) in snippets.iter().enumerate() { + let result = self.execute_single(snippet, index)?; + results.push(result); + } + Ok(results) + } + + fn execute_single( + &self, + snippet: &RunnableCodeBlock, + index: usize, + ) -> Result { + let temp_dir = + tempdir().context("failed to create temporary workspace for doc snippet execution")?; + let project_dir = Utf8PathBuf::from_path_buf(temp_dir.path().to_path_buf()) + .map_err(|path| anyhow!("path `{}` is not UTF-8 encoded", path.display()))?; + + self.write_manifest(&project_dir, index)?; + self.write_lib_cairo(&project_dir, snippet)?; + + let (print_output, program_output) = self.run_execute(&project_dir, index)?; + + Ok(CodeBlockExecutionResult { + code_block_id: snippet.code_block_id.clone(), + print_output, + program_output, + }) + } + + // TODO: consider using `ProjectBuilder` instead. + // - multiple snippets per package + // - or multiple packages with snippets in a workspace with common dep + fn write_manifest(&self, dir: &Utf8Path, index: usize) -> Result<()> { + let package_name = &self.package_metadata.name; + let package_dir = self + .package_metadata + .manifest_path + .parent() + .context("package manifest path has no parent directory")?; + + let name = self.snippet_name(index); + let dependency_path = format!("\"{}\"", package_dir); + let manifest = formatdoc! {r#" + [package] + name = "{name}" + version = "0.1.0" + edition = "2024_07" + + [dependencies] + {package_name} = {{ path = {dependency_path} }} + cairo_execute = "{CAIRO_VERSION}" + + [cairo] + enable-gas = false + + [executable] + "#}; + fs::write(dir.join("Scarb.toml"), manifest).context("failed to write snippet manifest")?; + Ok(()) + } + + fn write_lib_cairo(&self, dir: &Utf8Path, snippet: &RunnableCodeBlock) -> Result<()> { + let package_name = &self.package_metadata.name; + let src_dir = dir.join("src"); + fs::create_dir_all(&src_dir).context("failed to create snippet src directory")?; + + let mut body = String::new(); + for line in snippet.code.lines() { + if line.trim().is_empty() { + body.push_str(" \n"); + } else { + body.push_str(" "); + body.push_str(line); + body.push('\n'); + } + } + + let lib_cairo = formatdoc! {r#" + use {package_name}::*; + + #[executable] + fn snippet_main() {{ + {body} + }} + "# }; + fs::write(src_dir.join("lib.cairo"), lib_cairo) + .context("failed to write snippet lib.cairo")?; + Ok(()) + } + + fn run_execute(&self, project_dir: &Utf8Path, index: usize) -> Result<(String, String)> { + let target_dir = project_dir.join("target"); + let output_dir = target_dir.join("execute").join(self.snippet_name(index)); + create_output_dir(output_dir.as_std_path())?; + let (output_dir, execution_id) = incremental_create_execution_output_dir(&output_dir)?; + + ScarbCommand::new() + .arg("execute") + // .args(["--executable-function", "snippet_main"]) + .arg("--save-program-output") + .arg("--save-print-output") + .current_dir(project_dir) + .env("SCARB_EXECUTION_ID", execution_id.to_string()) + .env("SCARB_TARGET_DIR", target_dir.as_str()) + .env("SCARB_UI_VERBOSITY", self.ui.verbosity().to_string()) + .env( + "SCARB_MANIFEST_PATH", + project_dir.join("Scarb.toml").as_str(), + ) + .run() + .with_context(|| "execution failed")?; + + let print_output_file = output_dir.join(EXECUTE_PRINT_OUTPUT_FILENAME); + let print_output = fs::read_to_string(&print_output_file).with_context(|| { + format!( + "failed to read execution print output from file: {}", + print_output_file + ) + })?; + let program_output_file = output_dir.join(EXECUTE_PROGRAM_OUTPUT_FILENAME); + let program_output = fs::read_to_string(&program_output_file).with_context(|| { + format!( + "failed to read program output from file: {}", + program_output_file + ) + })?; + Ok(( + print_output.trim().to_string(), + program_output.trim().to_string(), + )) + } + + fn snippet_name(&self, index: usize) -> String { + let package_name = &self.package_metadata.name; + format!("{package_name}_snippet_{index}") + } +} + +pub fn collect_runnable_code_blocks(crate_: &Crate<'_>) -> Vec { + let mut runnable_code_blocks = Vec::new(); + collect_from_module(&crate_.root_module, &mut runnable_code_blocks); + // TODO: should these be ignored? + for module in &crate_.foreign_crates { + collect_from_module(module, &mut runnable_code_blocks); + } + runnable_code_blocks +} + +fn collect_from_module(module: &Module<'_>, runnable_code_blocks: &mut Vec) { + for item_data in module.get_all_item_ids().values() { + collect_from_item_data(item_data, runnable_code_blocks); + } + for item_data in module.pub_uses.get_all_item_ids().values() { + collect_from_item_data(item_data, runnable_code_blocks); + } +} + +fn collect_from_item_data( + item_data: &ItemData<'_>, + runnable_code_blocks: &mut Vec, +) { + for block in &item_data.code_blocks { + if block.is_runnable() { + runnable_code_blocks.push(RunnableCodeBlock { + code: block.code.to_string(), + code_block_id: block.id.clone(), + }); + } + } +} diff --git a/extensions/scarb-doc/src/types/module_type.rs b/extensions/scarb-doc/src/types/module_type.rs index 38a136b51..8dc92d28d 100644 --- a/extensions/scarb-doc/src/types/module_type.rs +++ b/extensions/scarb-doc/src/types/module_type.rs @@ -240,6 +240,53 @@ impl<'db> ModulePubUses<'db> { self.use_submodules.extend(use_submodules); self.use_macro_declarations.extend(use_macro_declarations); } + + pub(crate) fn get_all_item_ids(&self) -> HashMap, &ItemData<'_>> { + let mut ids: HashMap = HashMap::default(); + + self.use_constants.iter().for_each(|item| { + ids.insert(item.item_data.id, &item.item_data); + }); + self.use_free_functions.iter().for_each(|item| { + ids.insert(item.item_data.id, &item.item_data); + }); + self.use_module_type_aliases.iter().for_each(|item| { + ids.insert(item.item_data.id, &item.item_data); + }); + self.use_impl_aliases.iter().for_each(|item| { + ids.insert(item.item_data.id, &item.item_data); + }); + self.use_extern_types.iter().for_each(|item| { + ids.insert(item.item_data.id, &item.item_data); + }); + self.use_extern_functions.iter().for_each(|item| { + ids.insert(item.item_data.id, &item.item_data); + }); + self.use_macro_declarations.iter().for_each(|item| { + ids.insert(item.item_data.id, &item.item_data); + }); + self.use_structs.iter().for_each(|struct_| { + ids.insert(struct_.item_data.id, &struct_.item_data); + ids.extend(struct_.get_all_item_ids()); + }); + self.use_enums.iter().for_each(|enum_| { + ids.insert(enum_.item_data.id, &enum_.item_data); + ids.extend(enum_.get_all_item_ids()); + }); + self.use_traits.iter().for_each(|trait_| { + ids.insert(trait_.item_data.id, &trait_.item_data); + ids.extend(trait_.get_all_item_ids()); + }); + self.use_impl_defs.iter().for_each(|impl_| { + ids.insert(impl_.item_data.id, &impl_.item_data); + ids.extend(impl_.get_all_item_ids()); + }); + self.use_submodules.iter().for_each(|sub_module| { + ids.extend(sub_module.get_all_item_ids()); + }); + + ids + } } macro_rules! define_insert_function { From fac5939317755e1118e383c8a5bebe2f6039d01d Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Tue, 25 Nov 2025 04:36:10 +0300 Subject: [PATCH 04/32] lock --- Cargo.lock | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 01dc0e2c7..a11ce07b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4086,12 +4086,11 @@ dependencies = [ [[package]] name = "http" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] From 1bb3fa1e69836b14cd1702440109565f113b909e Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Tue, 25 Nov 2025 12:17:02 +0300 Subject: [PATCH 05/32] add test --- extensions/scarb-doc/tests/code/code_12.cairo | 33 +++++++++++++ .../tests/data/runnable_snippets/book.toml | 19 +++++++ .../data/runnable_snippets/src/SUMMARY.md | 5 ++ .../runnable_snippets/src/hello_world-bar.md | 12 +++++ .../runnable_snippets/src/hello_world-foo.md | 12 +++++ .../src/hello_world-foo_bar.md | 18 +++++++ .../src/hello_world-free_functions.md | 8 +++ .../data/runnable_snippets/src/hello_world.md | 12 +++++ .../scarb-doc/tests/runnable_code_blocks.rs | 49 +++++++++++++++++++ 9 files changed, 168 insertions(+) create mode 100644 extensions/scarb-doc/tests/code/code_12.cairo create mode 100644 extensions/scarb-doc/tests/data/runnable_snippets/book.toml create mode 100644 extensions/scarb-doc/tests/data/runnable_snippets/src/SUMMARY.md create mode 100644 extensions/scarb-doc/tests/data/runnable_snippets/src/hello_world-bar.md create mode 100644 extensions/scarb-doc/tests/data/runnable_snippets/src/hello_world-foo.md create mode 100644 extensions/scarb-doc/tests/data/runnable_snippets/src/hello_world-foo_bar.md create mode 100644 extensions/scarb-doc/tests/data/runnable_snippets/src/hello_world-free_functions.md create mode 100644 extensions/scarb-doc/tests/data/runnable_snippets/src/hello_world.md create mode 100644 extensions/scarb-doc/tests/runnable_code_blocks.rs diff --git a/extensions/scarb-doc/tests/code/code_12.cairo b/extensions/scarb-doc/tests/code/code_12.cairo new file mode 100644 index 000000000..c5b20c2d5 --- /dev/null +++ b/extensions/scarb-doc/tests/code/code_12.cairo @@ -0,0 +1,33 @@ + /// Function that prints "foo" to stdout with endline. + /// Can invoke it like that: + /// ```runnable + /// foo(); + /// ``` + pub fn foo() { + println!("foo"); + } + + /// Function that prints "bar" to stdout with endline. + /// Can invoke it like that: + /// ```cairo + /// bar(); + /// ``` + pub fn bar() { + println!("bar"); + } + + /// Function that calls both foo and bar functions. + /// Can invoke it like that: + /// ```cairo,runnable + /// foo_bar(); + /// ``` + pub fn foo_bar() { + foo(); + bar(); + } + + + /// Main function that cairo runs as a binary entrypoint. + fn main() { + println!("hello_world"); + } diff --git a/extensions/scarb-doc/tests/data/runnable_snippets/book.toml b/extensions/scarb-doc/tests/data/runnable_snippets/book.toml new file mode 100644 index 000000000..cbcfa69da --- /dev/null +++ b/extensions/scarb-doc/tests/data/runnable_snippets/book.toml @@ -0,0 +1,19 @@ +[book] +authors = [""] +language = "en" +multilingual = false +src = "src" +title = "hello_world - Cairo" + +[output.html] +no-section-label = true + +[output.html.playground] +runnable = false + +[output.html.fold] +enable = true +level = 0 + +[output.html.code.hidelines] +cairo = "#" diff --git a/extensions/scarb-doc/tests/data/runnable_snippets/src/SUMMARY.md b/extensions/scarb-doc/tests/data/runnable_snippets/src/SUMMARY.md new file mode 100644 index 000000000..038151544 --- /dev/null +++ b/extensions/scarb-doc/tests/data/runnable_snippets/src/SUMMARY.md @@ -0,0 +1,5 @@ +- [hello_world](./hello_world.md) + - [Free functions](./hello_world-free_functions.md) + - [foo](./hello_world-foo.md) + - [bar](./hello_world-bar.md) + - [foo_bar](./hello_world-foo_bar.md) diff --git a/extensions/scarb-doc/tests/data/runnable_snippets/src/hello_world-bar.md b/extensions/scarb-doc/tests/data/runnable_snippets/src/hello_world-bar.md new file mode 100644 index 000000000..7fe0ce410 --- /dev/null +++ b/extensions/scarb-doc/tests/data/runnable_snippets/src/hello_world-bar.md @@ -0,0 +1,12 @@ +# bar + +Function that prints "bar" to stdout with endline. +Can invoke it like that: +```cairo + bar(); +``` + +Fully qualified path: [hello_world](./hello_world.md)::[bar](./hello_world-bar.md) + +
pub fn bar()
+ diff --git a/extensions/scarb-doc/tests/data/runnable_snippets/src/hello_world-foo.md b/extensions/scarb-doc/tests/data/runnable_snippets/src/hello_world-foo.md new file mode 100644 index 000000000..264290a17 --- /dev/null +++ b/extensions/scarb-doc/tests/data/runnable_snippets/src/hello_world-foo.md @@ -0,0 +1,12 @@ +# foo + +Function that prints "foo" to stdout with endline. +Can invoke it like that: +```runnable +foo(); +``` + +Fully qualified path: [hello_world](./hello_world.md)::[foo](./hello_world-foo.md) + +
pub fn foo()
+ diff --git a/extensions/scarb-doc/tests/data/runnable_snippets/src/hello_world-foo_bar.md b/extensions/scarb-doc/tests/data/runnable_snippets/src/hello_world-foo_bar.md new file mode 100644 index 000000000..4400713c3 --- /dev/null +++ b/extensions/scarb-doc/tests/data/runnable_snippets/src/hello_world-foo_bar.md @@ -0,0 +1,18 @@ +# foo_bar + +Function that calls both foo and bar functions. +Can invoke it like that: +```cairo,runnable +foo_bar(); +``` +Output: +``` +foo +bar +``` + + +Fully qualified path: [hello_world](./hello_world.md)::[foo_bar](./hello_world-foo_bar.md) + +
pub fn foo_bar()
+ diff --git a/extensions/scarb-doc/tests/data/runnable_snippets/src/hello_world-free_functions.md b/extensions/scarb-doc/tests/data/runnable_snippets/src/hello_world-free_functions.md new file mode 100644 index 000000000..336c0f02b --- /dev/null +++ b/extensions/scarb-doc/tests/data/runnable_snippets/src/hello_world-free_functions.md @@ -0,0 +1,8 @@ + +## [Free functions](./hello_world-free_functions.md) + +| | | +|:---|:---| +| [foo](./hello_world-foo.md) | Function that prints "foo" to stdout with endline. Can invoke it like that:... | +| [bar](./hello_world-bar.md) | Function that prints "bar" to stdout with endline. Can invoke it like that:... | +| [foo_bar](./hello_world-foo_bar.md) | Function that calls both foo and bar functions. Can invoke it like that:... | diff --git a/extensions/scarb-doc/tests/data/runnable_snippets/src/hello_world.md b/extensions/scarb-doc/tests/data/runnable_snippets/src/hello_world.md new file mode 100644 index 000000000..404190827 --- /dev/null +++ b/extensions/scarb-doc/tests/data/runnable_snippets/src/hello_world.md @@ -0,0 +1,12 @@ +# hello_world + +Fully qualified path: [hello_world](./hello_world.md) + + +## [Free functions](./hello_world-free_functions.md) + +| | | +|:---|:---| +| [foo](./hello_world-foo.md) | Function that prints "foo" to stdout with endline. Can invoke it like that:... | +| [bar](./hello_world-bar.md) | Function that prints "bar" to stdout with endline. Can invoke it like that:... | +| [foo_bar](./hello_world-foo_bar.md) | Function that calls both foo and bar functions. Can invoke it like that:... | diff --git a/extensions/scarb-doc/tests/runnable_code_blocks.rs b/extensions/scarb-doc/tests/runnable_code_blocks.rs new file mode 100644 index 000000000..a9f717796 --- /dev/null +++ b/extensions/scarb-doc/tests/runnable_code_blocks.rs @@ -0,0 +1,49 @@ +use assert_fs::TempDir; +use indoc::formatdoc; +use scarb_test_support::command::Scarb; +// use scarb_test_support::filesystem::dump_temp; +use scarb_test_support::project_builder::ProjectBuilder; +mod markdown_target; +use markdown_target::MarkdownTargetChecker; + +const CODE_WITH_SNIPPETS: &str = include_str!("code/code_12.cairo"); +const EXPECTED_CODE_WITH_SNIPPETS_PATH: &str = "tests/data/runnable_snippets"; + +#[test] +fn supports_runnable_examples() { + let t = TempDir::new().unwrap(); + ProjectBuilder::start() + .name("hello_world") + .lib_cairo(CODE_WITH_SNIPPETS) + .build(&t); + + Scarb::quick_snapbox() + .arg("doc") + .args(["--output-format", "markdown"]) + .arg("--build") + .current_dir(&t) + .assert() + .success() + .stdout_eq(formatdoc! {r#" + [..] Executing snippet #0 from `hello_world::foo_bar` + [..] Compiling hello_world_snippet_0 v0.1.0 ([..]) + [..] Finished `dev` profile target(s) in [..] + [..] Executing hello_world_snippet_0 + foo + bar + Saving output to: target/execute/hello_world_snippet_0/execution1 + Saving output to: target/doc/hello_world + Saving build output to: target/doc/hello_world/book + + Run the following to see the results:[..] + `mdbook serve target/doc/hello_world` + + Or open the following in your browser:[..] + `[..]/target/doc/hello_world/book/index.html` + "#}); + + MarkdownTargetChecker::lenient() + .actual(t.path().join("target/doc/hello_world").to_str().unwrap()) + .expected(EXPECTED_CODE_WITH_SNIPPETS_PATH) + .assert_all_files_match(); +} From cd9e7ad69e330c73aae073c98f994a8b2061e2b7 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Tue, 25 Nov 2025 12:17:55 +0300 Subject: [PATCH 06/32] update `SHORT_DOCUMENTATION_AVOID_PREFIXES` to include `~~~` --- extensions/scarb-doc/src/docs_generation/markdown.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/scarb-doc/src/docs_generation/markdown.rs b/extensions/scarb-doc/src/docs_generation/markdown.rs index cbe943583..12a269d52 100644 --- a/extensions/scarb-doc/src/docs_generation/markdown.rs +++ b/extensions/scarb-doc/src/docs_generation/markdown.rs @@ -28,7 +28,8 @@ pub const GROUP_CHAPTER_PREFIX: &str = "- ###"; /// Prefixes that indicate the start of complex Markdown structures, /// such as tables. These should be avoided in brief documentation to maintain simple text /// formatting and prevent disruption of the layout. -const SHORT_DOCUMENTATION_AVOID_PREFIXES: &[&str] = &["#", "\n\n", "```", "- ", "1. ", "{{#"]; +const SHORT_DOCUMENTATION_AVOID_PREFIXES: &[&str] = + &["#", "\n\n", "```", "~~~", "- ", "1. ", "{{#"]; pub struct MarkdownContent { book_toml: String, From fb3793e6bb45c540826e8246f21a2f0f55cfa0b8 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Tue, 25 Nov 2025 12:30:11 +0300 Subject: [PATCH 07/32] misc --- extensions/scarb-doc/src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/scarb-doc/src/main.rs b/extensions/scarb-doc/src/main.rs index ec4922b62..d90100c5f 100644 --- a/extensions/scarb-doc/src/main.rs +++ b/extensions/scarb-doc/src/main.rs @@ -140,8 +140,8 @@ impl OutputEmit { let execution_results = if snippet_execution_enabled { let snippets = collect_runnable_code_blocks(&package.crate_); if !snippets.is_empty() { - let executor = SnippetRunner::new(&package.package_metadata, ui.clone()); - let execution_results = executor.execute(&snippets)?; + let runner = SnippetRunner::new(&package.package_metadata, ui.clone()); + let execution_results = runner.execute(&snippets)?; Some(execution_results) } else { Some(vec![]) From 6d0a56bf7a93b53b7403f8f3840e9cc66fdcb508 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Tue, 25 Nov 2025 13:35:40 +0300 Subject: [PATCH 08/32] resolve issues (post-rebase) --- extensions/scarb-doc/tests/runnable_code_blocks.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/scarb-doc/tests/runnable_code_blocks.rs b/extensions/scarb-doc/tests/runnable_code_blocks.rs index a9f717796..8470c920a 100644 --- a/extensions/scarb-doc/tests/runnable_code_blocks.rs +++ b/extensions/scarb-doc/tests/runnable_code_blocks.rs @@ -17,7 +17,7 @@ fn supports_runnable_examples() { .lib_cairo(CODE_WITH_SNIPPETS) .build(&t); - Scarb::quick_snapbox() + Scarb::quick_command() .arg("doc") .args(["--output-format", "markdown"]) .arg("--build") From d7e62145dcf4420656c6d7962ac29dc27963ece6 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Tue, 25 Nov 2025 13:38:27 +0300 Subject: [PATCH 09/32] print execution status --- extensions/scarb-doc/src/runner.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/extensions/scarb-doc/src/runner.rs b/extensions/scarb-doc/src/runner.rs index baa316447..900e35400 100644 --- a/extensions/scarb-doc/src/runner.rs +++ b/extensions/scarb-doc/src/runner.rs @@ -12,6 +12,7 @@ use indoc::formatdoc; use scarb_build_metadata::CAIRO_VERSION; use scarb_metadata::{PackageMetadata, ScarbCommand}; use scarb_ui::Ui; +use scarb_ui::components::Status; use std::fs; use tempfile::tempdir; @@ -86,7 +87,7 @@ impl<'a> SnippetRunner<'a> { self.write_manifest(&project_dir, index)?; self.write_lib_cairo(&project_dir, snippet)?; - let (print_output, program_output) = self.run_execute(&project_dir, index)?; + let (print_output, program_output) = self.run_execute(&project_dir, index, snippet)?; Ok(CodeBlockExecutionResult { code_block_id: snippet.code_block_id.clone(), @@ -156,12 +157,25 @@ impl<'a> SnippetRunner<'a> { Ok(()) } - fn run_execute(&self, project_dir: &Utf8Path, index: usize) -> Result<(String, String)> { + fn run_execute( + &self, + project_dir: &Utf8Path, + index: usize, + snippet: &RunnableCodeBlock, + ) -> Result<(String, String)> { let target_dir = project_dir.join("target"); let output_dir = target_dir.join("execute").join(self.snippet_name(index)); create_output_dir(output_dir.as_std_path())?; let (output_dir, execution_id) = incremental_create_execution_output_dir(&output_dir)?; + self.ui.print(Status::new( + "Executing", + format!( + "snippet #{} from `{}`", + index, snippet.code_block_id.item_full_path + ) + .as_str(), + )); ScarbCommand::new() .arg("execute") // .args(["--executable-function", "snippet_main"]) From 9c278eafa384e333b13d825686628cffe1263364 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 26 Nov 2025 20:48:01 +0300 Subject: [PATCH 10/32] initial refactor --- Cargo.lock | 3 + extensions/scarb-doc/src/code_blocks.rs | 99 +++++++++++------ extensions/scarb-doc/src/docs_generation.rs | 6 +- .../scarb-doc/src/docs_generation/markdown.rs | 4 +- .../src/docs_generation/markdown/summary.rs | 4 +- .../docs_generation/markdown/summary/files.rs | 10 +- .../markdown/summary/group_files.rs | 4 +- .../src/docs_generation/markdown/traits.rs | 24 ++--- extensions/scarb-doc/src/main.rs | 14 +-- extensions/scarb-doc/src/runner.rs | 100 ++++++++---------- extensions/scarb-doc/src/types/item_data.rs | 4 +- extensions/scarb-doc/src/types/other_types.rs | 21 +++- 12 files changed, 164 insertions(+), 129 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a11ce07b7..75abe0b17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6926,11 +6926,13 @@ dependencies = [ "cairo-lang-utils", "camino", "clap", + "create-output-dir", "expect-test", "indoc", "itertools 0.14.0", "mimalloc", "salsa", + "scarb-build-metadata", "scarb-extensions-cli", "scarb-metadata 1.15.1", "scarb-test-support", @@ -6938,6 +6940,7 @@ dependencies = [ "serde", "serde_json", "snapbox", + "tempfile", "thiserror 2.0.17", ] diff --git a/extensions/scarb-doc/src/code_blocks.rs b/extensions/scarb-doc/src/code_blocks.rs index 7ad0376e6..82110024e 100644 --- a/extensions/scarb-doc/src/code_blocks.rs +++ b/extensions/scarb-doc/src/code_blocks.rs @@ -2,12 +2,12 @@ use cairo_lang_doc::parser::DocumentationCommentToken; use std::str::from_utf8; #[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct DocCodeBlockId { +pub struct CodeBlockId { pub item_full_path: String, pub close_token_idx: usize, } -impl DocCodeBlockId { +impl CodeBlockId { pub fn new(item_full_path: String, close_token_idx: usize) -> Self { Self { item_full_path, @@ -16,45 +16,75 @@ impl DocCodeBlockId { } } +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum CodeBlockAttribute { + Cairo, + Runnable, + Ignore, + NoRun, + Other(String), +} + +impl From<&str> for CodeBlockAttribute { + fn from(string: &str) -> Self { + match string.to_lowercase().as_str() { + "cairo" => CodeBlockAttribute::Cairo, + "runnable" => CodeBlockAttribute::Runnable, + "ignore" => CodeBlockAttribute::Ignore, + "no_run" | "no-run" => CodeBlockAttribute::NoRun, + _ => CodeBlockAttribute::Other(string.to_string()), + } + } +} + /// Represents code block extracted from doc comments. #[derive(Debug, Clone, PartialEq)] -pub struct DocCodeBlock { - pub id: DocCodeBlockId, - pub code: String, - pub language: String, - pub attributes: Vec, +pub struct CodeBlock { + pub id: CodeBlockId, + pub content: String, + pub attributes: Vec, } -impl DocCodeBlock { - pub fn new(id: DocCodeBlockId, code: String, info_string: &String) -> Self { - let (language, attributes) = Self::parse_info_string(info_string); +impl CodeBlock { + pub fn new(id: CodeBlockId, content: String, info_string: &String) -> Self { + let attributes = Self::parse_attributes(info_string); Self { id, - code, - language, + content, attributes, } } - pub fn is_runnable(&self) -> bool { - self.language == "cairo" && self.attributes.iter().any(|attr| attr == "runnable") + // TODO: default to Cairo unless specified otherwise? + fn is_cairo(&self) -> bool { + if self.attributes.contains(&CodeBlockAttribute::Cairo) { + return true; + } + // Assume unknown attributes imply non-Cairo code. + // !self.attributes.iter().any(|attr| matches!(attr, CodeBlockAttribute::Other(_))) + false } - /// Parses info string into language and attributes. The results are lowercased. - fn parse_info_string(info_string: &str) -> (String, Vec) { - let parts: Vec<_> = info_string - .trim() - .split(',') - .map(|s| s.trim().to_ascii_lowercase()) - .filter(|s| !s.is_empty()) - .collect(); + // TODO: consider runnable by default unless specified otherwise? + pub fn should_run(&self) -> bool { + self.is_cairo() && self.attributes.contains(&CodeBlockAttribute::Runnable) + // && !self.attributes.contains(&CodeBlockAttribute::Ignore) + // && !self.attributes.contains(&CodeBlockAttribute::NoRun) + } - if parts.is_empty() { - return (String::new(), Vec::new()); - } - let language = parts[0].to_string(); - let attributes = parts[1..].iter().map(|s| s.to_string()).collect(); - (language, attributes) + // TODO: implement building examples without running them + #[allow(unused)] + pub fn should_build(&self) -> bool { + self.is_cairo() && !self.attributes.contains(&CodeBlockAttribute::Ignore) + } + + fn parse_attributes(info_string: &str) -> Vec { + info_string + .split(',') + .map(|attr| attr.trim()) + .filter(|attr| !attr.is_empty()) + .map(Into::into) + .collect() } } @@ -62,7 +92,7 @@ impl DocCodeBlock { pub fn collect_code_blocks( doc_tokens: &Option>, full_path: &str, -) -> Vec { +) -> Vec { let Some(tokens) = doc_tokens else { return Vec::new(); }; @@ -91,14 +121,12 @@ pub fn collect_code_blocks( Some(ref opening) => { if is_matching_closing_fence(content, opening.char, opening.len) { let end_idx = idx; - let body = get_block_body(tokens, opening.token_idx + 1, end_idx) - .trim() - .to_string(); + let body = get_block_body(tokens, opening.token_idx + 1, end_idx); // Skip empty code blocks. - let id = DocCodeBlockId::new(full_path.to_string(), idx); + let id = CodeBlockId::new(full_path.to_string(), end_idx); if !body.is_empty() { - code_blocks.push(DocCodeBlock::new( + code_blocks.push(CodeBlock::new( id, body.to_string(), &opening.info_string, @@ -158,7 +186,8 @@ fn is_matching_closing_fence(content: &str, opening_char: u8, opening_len: usize .all(|&b| matches!(b, b' ' | b'\t' | b'\r' | b'\n')) } -/// Copied from https://github.com/pulldown-cmark/pulldown-cmark/blob/a574ea8a5e6fda7bc26542a612130a2b458a68a7/pulldown-cmark/src/scanners.rs#L744 +/// Copied from `pulldown-cmark`: +/// https://github.com/pulldown-cmark/pulldown-cmark/blob/a574ea8a5e6fda7bc26542a612130a2b458a68a7/pulldown-cmark/src/scanners.rs#L744 fn scan_code_fence(data: &[u8]) -> Option<(usize, u8)> { let c = *data.first()?; if !(c == b'`' || c == b'~') { diff --git a/extensions/scarb-doc/src/docs_generation.rs b/extensions/scarb-doc/src/docs_generation.rs index 9ba44e97f..17245b3c9 100644 --- a/extensions/scarb-doc/src/docs_generation.rs +++ b/extensions/scarb-doc/src/docs_generation.rs @@ -1,4 +1,4 @@ -use crate::code_blocks::DocCodeBlock; +use crate::code_blocks::CodeBlock; use crate::location_links::DocLocationLink; use crate::types::module_type::Module; use crate::types::other_types::{ @@ -80,7 +80,7 @@ pub trait DocItem { fn doc_location_links(&self) -> &Vec; fn markdown_formatted_path(&self) -> String; fn group_name(&self) -> &Option; - fn code_blocks(&self) -> &Vec; + fn code_blocks(&self) -> &Vec; } macro_rules! impl_doc_item { @@ -116,7 +116,7 @@ macro_rules! impl_doc_item { &self.item_data.group } - fn code_blocks(&self) -> &Vec { + fn code_blocks(&self) -> &Vec { &self.item_data.code_blocks } } diff --git a/extensions/scarb-doc/src/docs_generation/markdown.rs b/extensions/scarb-doc/src/docs_generation/markdown.rs index 12a269d52..abfb9ed71 100644 --- a/extensions/scarb-doc/src/docs_generation/markdown.rs +++ b/extensions/scarb-doc/src/docs_generation/markdown.rs @@ -14,7 +14,7 @@ pub mod traits; use crate::docs_generation::common::{ GeneratedFile, OutputFilesExtension, SummaryIndexMap, SummaryListItem, }; -use crate::runner::CodeBlockExecutionResult; +use crate::runner::ExecutionResult; use std::ops::Add; const BASE_HEADER_LEVEL: usize = 1; @@ -42,7 +42,7 @@ impl MarkdownContent { pub fn from_crate( package_information: &PackageInformation, format: OutputFilesExtension, - execution_results: Option>, + execution_results: Option>, ) -> Result { let (summary, doc_files) = generate_summary_file_content(&package_information.crate_, format, execution_results)?; diff --git a/extensions/scarb-doc/src/docs_generation/markdown/summary.rs b/extensions/scarb-doc/src/docs_generation/markdown/summary.rs index 33a187ed6..473a181f8 100644 --- a/extensions/scarb-doc/src/docs_generation/markdown/summary.rs +++ b/extensions/scarb-doc/src/docs_generation/markdown/summary.rs @@ -13,7 +13,7 @@ use crate::docs_generation::markdown::summary::files::{ }; use crate::docs_generation::markdown::traits::{MarkdownDocItem, TopLevelMarkdownDocItem}; use crate::docs_generation::markdown::{BASE_HEADER_LEVEL, SummaryIndexMap}; -use crate::runner::CodeBlockExecutionResult; +use crate::runner::ExecutionResult; use crate::types::crate_type::Crate; use anyhow::Result; use group_files::generate_global_groups_summary_files; @@ -21,7 +21,7 @@ use group_files::generate_global_groups_summary_files; pub fn generate_summary_file_content( crate_: &Crate, output_format: OutputFilesExtension, - execution_results: Option>, + execution_results: Option>, ) -> Result<(SummaryIndexMap, Vec<(String, String)>)> { let mut summary_index_map = SummaryIndexMap::new(); let context = MarkdownGenerationContext::from_crate(crate_, output_format); diff --git a/extensions/scarb-doc/src/docs_generation/markdown/summary/files.rs b/extensions/scarb-doc/src/docs_generation/markdown/summary/files.rs index 05ebdd734..241e30f89 100644 --- a/extensions/scarb-doc/src/docs_generation/markdown/summary/files.rs +++ b/extensions/scarb-doc/src/docs_generation/markdown/summary/files.rs @@ -6,7 +6,7 @@ use crate::docs_generation::markdown::traits::{ }; use crate::docs_generation::markdown::{BASE_HEADER_LEVEL, BASE_MODULE_CHAPTER_PREFIX}; use crate::docs_generation::{DocItem, TopLevelItems, common}; -use crate::runner::CodeBlockExecutionResult; +use crate::runner::ExecutionResult; use crate::types::module_type::Module; use crate::types::other_types::{ Constant, Enum, ExternFunction, ExternType, FreeFunction, Impl, ImplAlias, MacroDeclaration, @@ -39,7 +39,7 @@ pub fn generate_modules_summary_files( module: &Module, context: &MarkdownGenerationContext, summary_index_map: &SummaryIndexMap, - execution_results: Option>, + execution_results: Option>, ) -> Result> { let mut top_level_items = TopLevelItems::default(); let Module { @@ -107,7 +107,7 @@ pub fn generate_foreign_crates_summary_files( foreign_modules: &Vec, context: &MarkdownGenerationContext, summary_index_map: &SummaryIndexMap, - execution_results: Option>, + execution_results: Option>, ) -> Result> { let mut summary_files = vec![]; @@ -167,7 +167,7 @@ pub fn generate_doc_files_for_module_items( top_level_items: &TopLevelItems, context: &MarkdownGenerationContext, summary_index_map: &SummaryIndexMap, - execution_results: Option>, + execution_results: Option>, ) -> Result> { Ok(chain!( generate_top_level_docs_contents( @@ -250,7 +250,7 @@ fn generate_top_level_docs_contents( items: &[&impl TopLevelMarkdownDocItem], context: &MarkdownGenerationContext, summary_index_map: &SummaryIndexMap, - execution_results: Option>, + execution_results: Option>, ) -> Result> { items .iter() diff --git a/extensions/scarb-doc/src/docs_generation/markdown/summary/group_files.rs b/extensions/scarb-doc/src/docs_generation/markdown/summary/group_files.rs index f99ffcb38..4daacb476 100644 --- a/extensions/scarb-doc/src/docs_generation/markdown/summary/group_files.rs +++ b/extensions/scarb-doc/src/docs_generation/markdown/summary/group_files.rs @@ -8,7 +8,7 @@ use crate::docs_generation::markdown::summary::files::{ use crate::docs_generation::markdown::traits::generate_markdown_table_summary_for_top_level_subitems; use crate::docs_generation::common::SummaryIndexMap; -use crate::runner::CodeBlockExecutionResult; +use crate::runner::ExecutionResult; use crate::types::groups::Group; use itertools::Itertools; @@ -29,7 +29,7 @@ pub fn generate_global_groups_summary_files( groups: &[Group], context: &MarkdownGenerationContext, summary_index_map: &SummaryIndexMap, - execution_results: Option>, + execution_results: Option>, ) -> anyhow::Result> { let mut doc_files: Vec<(String, String)> = Vec::new(); diff --git a/extensions/scarb-doc/src/docs_generation/markdown/traits.rs b/extensions/scarb-doc/src/docs_generation/markdown/traits.rs index 9620fe144..f019bd697 100644 --- a/extensions/scarb-doc/src/docs_generation/markdown/traits.rs +++ b/extensions/scarb-doc/src/docs_generation/markdown/traits.rs @@ -4,7 +4,7 @@ use crate::docs_generation::markdown::{ SHORT_DOCUMENTATION_LEN, }; use crate::docs_generation::{DocItem, PrimitiveDocItem, SubPathDocItem, TopLevelDocItem, common}; -use crate::runner::CodeBlockExecutionResult; +use crate::runner::ExecutionResult; use crate::types::groups::Group; use crate::types::item_data::{ItemData, SubItemData}; use crate::types::module_type::{Module, ModulePubUses}; @@ -84,7 +84,7 @@ macro_rules! impl_markdown_doc_item { header_level: usize, item_suffix: Option, summary_index_map: &SummaryIndexMap, - execution_results: Option>, + execution_results: Option>, ) -> Result { let mut markdown = String::new(); @@ -138,7 +138,7 @@ pub trait MarkdownDocItem: DocItem { header_level: usize, item_suffix: Option, summary_index_map: &SummaryIndexMap, - execution_results: Option>, + execution_results: Option>, ) -> Result; fn get_short_documentation(&self, context: &MarkdownGenerationContext) -> String { @@ -187,7 +187,7 @@ pub trait MarkdownDocItem: DocItem { fn get_documentation( &self, context: &MarkdownGenerationContext, - execution_results: Option>, + execution_results: Option>, ) -> Option { self.doc().as_ref().map(|doc_tokens| { // TODO: filter out execution results that do not belong to this item @@ -245,7 +245,7 @@ where header_level: usize, _item_suffix: Option, summary_index_map: &SummaryIndexMap, - execution_results: Option>, + execution_results: Option>, ) -> Result { generate_markdown_from_item_data( self, @@ -265,7 +265,7 @@ impl<'db> MarkdownDocItem for Enum<'db> { header_level: usize, _item_suffix: Option, summary_index_map: &SummaryIndexMap, - execution_results: Option>, + execution_results: Option>, ) -> Result { let mut markdown = generate_markdown_from_item_data( self, @@ -296,7 +296,7 @@ impl<'db> MarkdownDocItem for Impl<'db> { header_level: usize, _item_suffix: Option, summary_index_map: &SummaryIndexMap, - execution_results: Option>, + execution_results: Option>, ) -> Result { let mut markdown = generate_markdown_from_item_data( self, @@ -430,7 +430,7 @@ impl<'db> MarkdownDocItem for Module<'db> { header_level: usize, _item_suffix: Option, summary_index_map: &SummaryIndexMap, - execution_results: Option>, + execution_results: Option>, ) -> Result { let mut markdown = generate_markdown_from_item_data( self, @@ -533,7 +533,7 @@ impl<'db> MarkdownDocItem for Struct<'db> { header_level: usize, _item_suffix: Option, summary_index_map: &SummaryIndexMap, - execution_results: Option>, + execution_results: Option>, ) -> Result { let mut markdown = generate_markdown_from_item_data( self, @@ -565,7 +565,7 @@ impl<'db> MarkdownDocItem for Trait<'db> { header_level: usize, _item_suffix: Option, summary_index_map: &SummaryIndexMap, - execution_results: Option>, + execution_results: Option>, ) -> Result { let mut markdown = generate_markdown_from_item_data( self, @@ -841,7 +841,7 @@ fn generate_markdown_for_subitems( header_level: usize, suffix_calculator: &mut ItemSuffixCalculator, summary_index_map: &SummaryIndexMap, - execution_results: Option>, + execution_results: Option>, ) -> Result { let mut markdown = String::new(); @@ -875,7 +875,7 @@ fn generate_markdown_from_item_data( header_level: usize, item_suffix: Option, summary_index_map: &SummaryIndexMap, - execution_results: Option>, + execution_results: Option>, ) -> Result { let mut markdown = String::new(); diff --git a/extensions/scarb-doc/src/main.rs b/extensions/scarb-doc/src/main.rs index d90100c5f..715b1bec1 100644 --- a/extensions/scarb-doc/src/main.rs +++ b/extensions/scarb-doc/src/main.rs @@ -7,7 +7,7 @@ use scarb_doc::docs_generation::common::OutputFilesExtension; use scarb_doc::docs_generation::markdown::{MarkdownContent, WorkspaceMarkdownBuilder}; use scarb_doc::errors::{MetadataCommandError, PackagesSerializationError}; use scarb_doc::metadata::get_target_dir; -use scarb_doc::runner::{SnippetRunner, collect_runnable_code_blocks}; +use scarb_doc::runner::{DocTestRunner, collect_runnable_code_blocks}; use scarb_doc::versioned_json_output::VersionedJsonOutput; use scarb_doc::{PackageInformation, generate_package_context, generate_package_information}; use scarb_extensions_cli::doc::{Args, OutputFormat}; @@ -136,12 +136,12 @@ impl OutputEmit { files_extension, } => { // TODO: refactor this - let snippet_execution_enabled = *build; - let execution_results = if snippet_execution_enabled { - let snippets = collect_runnable_code_blocks(&package.crate_); - if !snippets.is_empty() { - let runner = SnippetRunner::new(&package.package_metadata, ui.clone()); - let execution_results = runner.execute(&snippets)?; + let execution_enabled = *build; + let execution_results = if execution_enabled { + let runnable_code_blocks = collect_runnable_code_blocks(&package.crate_); + if !runnable_code_blocks.is_empty() { + let runner = DocTestRunner::new(&package.package_metadata, ui.clone()); + let execution_results = runner.execute(&runnable_code_blocks)?; Some(execution_results) } else { Some(vec![]) diff --git a/extensions/scarb-doc/src/runner.rs b/extensions/scarb-doc/src/runner.rs index 900e35400..267f6520e 100644 --- a/extensions/scarb-doc/src/runner.rs +++ b/extensions/scarb-doc/src/runner.rs @@ -1,15 +1,16 @@ -use crate::code_blocks::DocCodeBlockId; +use crate::code_blocks::{CodeBlock, CodeBlockId}; use crate::types::crate_type::Crate; use crate::types::module_type::Module; use crate::types::other_types::ItemData; use anyhow::{Context, Result, anyhow}; use camino::{Utf8Path, Utf8PathBuf}; -use create_output_dir::{ - EXECUTE_PRINT_OUTPUT_FILENAME, EXECUTE_PROGRAM_OUTPUT_FILENAME, create_output_dir, - incremental_create_execution_output_dir, -}; +use create_output_dir::create_output_dir; use indoc::formatdoc; use scarb_build_metadata::CAIRO_VERSION; +use scarb_execute_utils::{ + EXECUTE_PRINT_OUTPUT_FILENAME, EXECUTE_PROGRAM_OUTPUT_FILENAME, + incremental_create_execution_output_dir, +}; use scarb_metadata::{PackageMetadata, ScarbCommand}; use scarb_ui::Ui; use scarb_ui::components::Status; @@ -17,19 +18,13 @@ use std::fs; use tempfile::tempdir; #[derive(Debug, Clone)] -pub struct RunnableCodeBlock { - pub code_block_id: DocCodeBlockId, - pub code: String, -} - -#[derive(Debug, Clone)] -pub struct CodeBlockExecutionResult { - pub code_block_id: DocCodeBlockId, +pub struct ExecutionResult { + pub code_block_id: CodeBlockId, pub print_output: String, pub program_output: String, } -impl CodeBlockExecutionResult { +impl ExecutionResult { /// Formats the execution result as markdown with code blocks. pub fn format_as_markdown(&self) -> String { let mut output = String::new(); @@ -50,14 +45,14 @@ impl CodeBlockExecutionResult { } } -/// A runner for executing runnable code runnable_code_blocks extracted from documentation. -/// Uses `scarb execute` to run the runnable_code_blocks in isolated temporary workspaces. -pub struct SnippetRunner<'a> { +/// A runner for executing examples (code blocks) found in documentation. +/// Uses `scarb execute` and runs code blocks in isolated temporary workspaces. +pub struct DocTestRunner<'a> { package_metadata: &'a PackageMetadata, ui: Ui, } -impl<'a> SnippetRunner<'a> { +impl<'a> DocTestRunner<'a> { pub fn new(package_metadata: &'a PackageMetadata, ui: Ui) -> Self { Self { package_metadata, @@ -65,32 +60,28 @@ impl<'a> SnippetRunner<'a> { } } - pub fn execute(&self, snippets: &[RunnableCodeBlock]) -> Result> { + pub fn execute(&self, code_blocks: &[CodeBlock]) -> Result> { let mut results = Vec::new(); - for (index, snippet) in snippets.iter().enumerate() { - let result = self.execute_single(snippet, index)?; + for (index, code_block) in code_blocks.iter().enumerate() { + let result = self.execute_single(code_block, index)?; results.push(result); } Ok(results) } - fn execute_single( - &self, - snippet: &RunnableCodeBlock, - index: usize, - ) -> Result { + fn execute_single(&self, code_block: &CodeBlock, index: usize) -> Result { let temp_dir = tempdir().context("failed to create temporary workspace for doc snippet execution")?; let project_dir = Utf8PathBuf::from_path_buf(temp_dir.path().to_path_buf()) .map_err(|path| anyhow!("path `{}` is not UTF-8 encoded", path.display()))?; self.write_manifest(&project_dir, index)?; - self.write_lib_cairo(&project_dir, snippet)?; + self.write_lib_cairo(&project_dir, code_block)?; - let (print_output, program_output) = self.run_execute(&project_dir, index, snippet)?; + let (print_output, program_output) = self.run_execute(&project_dir, index, code_block)?; - Ok(CodeBlockExecutionResult { - code_block_id: snippet.code_block_id.clone(), + Ok(ExecutionResult { + code_block_id: code_block.id.clone(), print_output, program_output, }) @@ -107,7 +98,7 @@ impl<'a> SnippetRunner<'a> { .parent() .context("package manifest path has no parent directory")?; - let name = self.snippet_name(index); + let name = self.generated_package_name(index); let dependency_path = format!("\"{}\"", package_dir); let manifest = formatdoc! {r#" [package] @@ -124,17 +115,17 @@ impl<'a> SnippetRunner<'a> { [executable] "#}; - fs::write(dir.join("Scarb.toml"), manifest).context("failed to write snippet manifest")?; + fs::write(dir.join("Scarb.toml"), manifest).context("failed to write manifest for example")?; Ok(()) } - fn write_lib_cairo(&self, dir: &Utf8Path, snippet: &RunnableCodeBlock) -> Result<()> { + fn write_lib_cairo(&self, dir: &Utf8Path, code_block: &CodeBlock) -> Result<()> { let package_name = &self.package_metadata.name; let src_dir = dir.join("src"); - fs::create_dir_all(&src_dir).context("failed to create snippet src directory")?; + fs::create_dir_all(&src_dir).context("failed to create src directory")?; let mut body = String::new(); - for line in snippet.code.lines() { + for line in code_block.content.lines() { if line.trim().is_empty() { body.push_str(" \n"); } else { @@ -148,12 +139,12 @@ impl<'a> SnippetRunner<'a> { use {package_name}::*; #[executable] - fn snippet_main() {{ + fn main() {{ {body} }} "# }; fs::write(src_dir.join("lib.cairo"), lib_cairo) - .context("failed to write snippet lib.cairo")?; + .context("failed to write lib.cairo")?; Ok(()) } @@ -161,24 +152,22 @@ impl<'a> SnippetRunner<'a> { &self, project_dir: &Utf8Path, index: usize, - snippet: &RunnableCodeBlock, + code_block: &CodeBlock, ) -> Result<(String, String)> { let target_dir = project_dir.join("target"); - let output_dir = target_dir.join("execute").join(self.snippet_name(index)); + let output_dir = target_dir + .join("execute") + .join(self.generated_package_name(index)); create_output_dir(output_dir.as_std_path())?; let (output_dir, execution_id) = incremental_create_execution_output_dir(&output_dir)?; self.ui.print(Status::new( "Executing", - format!( - "snippet #{} from `{}`", - index, snippet.code_block_id.item_full_path - ) - .as_str(), + format!("example #{} from `{}`", index, code_block.id.item_full_path).as_str(), )); ScarbCommand::new() .arg("execute") - // .args(["--executable-function", "snippet_main"]) + // .args(["--executable-function", "main"]) .arg("--save-program-output") .arg("--save-print-output") .current_dir(project_dir) @@ -212,13 +201,14 @@ impl<'a> SnippetRunner<'a> { )) } - fn snippet_name(&self, index: usize) -> String { + fn generated_package_name(&self, index: usize) -> String { let package_name = &self.package_metadata.name; - format!("{package_name}_snippet_{index}") + format!("{package_name}_example_{index}") } } -pub fn collect_runnable_code_blocks(crate_: &Crate<'_>) -> Vec { +/// Collects all runnable `DocCodeBlock`s from the crate. +pub fn collect_runnable_code_blocks(crate_: &Crate<'_>) -> Vec { let mut runnable_code_blocks = Vec::new(); collect_from_module(&crate_.root_module, &mut runnable_code_blocks); // TODO: should these be ignored? @@ -228,7 +218,7 @@ pub fn collect_runnable_code_blocks(crate_: &Crate<'_>) -> Vec, runnable_code_blocks: &mut Vec) { +fn collect_from_module(module: &Module<'_>, runnable_code_blocks: &mut Vec) { for item_data in module.get_all_item_ids().values() { collect_from_item_data(item_data, runnable_code_blocks); } @@ -237,16 +227,10 @@ fn collect_from_module(module: &Module<'_>, runnable_code_blocks: &mut Vec, - runnable_code_blocks: &mut Vec, -) { +fn collect_from_item_data(item_data: &ItemData<'_>, runnable_code_blocks: &mut Vec) { for block in &item_data.code_blocks { - if block.is_runnable() { - runnable_code_blocks.push(RunnableCodeBlock { - code: block.code.to_string(), - code_block_id: block.id.clone(), - }); + if block.should_run() { + runnable_code_blocks.push(block.clone()); } } } diff --git a/extensions/scarb-doc/src/types/item_data.rs b/extensions/scarb-doc/src/types/item_data.rs index d59456d03..eecaa95b9 100644 --- a/extensions/scarb-doc/src/types/item_data.rs +++ b/extensions/scarb-doc/src/types/item_data.rs @@ -10,7 +10,7 @@ use cairo_lang_filesystem::ids::CrateId; use serde::Serialize; use serde::Serializer; use std::fmt::Debug; -use crate::code_blocks::{collect_code_blocks, DocCodeBlock}; +use crate::code_blocks::{collect_code_blocks, CodeBlock}; #[derive(Debug, Serialize, Clone)] pub struct ItemData<'db> { @@ -24,7 +24,7 @@ pub struct ItemData<'db> { pub signature: Option, pub full_path: String, #[serde(skip_serializing)] - pub code_blocks: Vec, + pub code_blocks: Vec, #[serde(skip_serializing)] pub doc_location_links: Vec, pub group: Option, diff --git a/extensions/scarb-doc/src/types/other_types.rs b/extensions/scarb-doc/src/types/other_types.rs index 41b535cc9..08bdd4fe9 100644 --- a/extensions/scarb-doc/src/types/other_types.rs +++ b/extensions/scarb-doc/src/types/other_types.rs @@ -1,7 +1,7 @@ use anyhow::Result; use crate::attributes::find_groups_from_attributes; -use crate::code_blocks::{DocCodeBlock, collect_code_blocks}; +use crate::code_blocks::{CodeBlock, collect_code_blocks}; use crate::db::ScarbDocDatabase; use crate::docs_generation::markdown::context::IncludedItems; use crate::docs_generation::markdown::traits::WithPath; @@ -22,6 +22,25 @@ use cairo_lang_syntax::node::ast; use serde::Serialize; use std::collections::HashMap; +#[derive(Debug, Serialize, Clone)] +pub struct ItemData<'db> { + #[serde(skip_serializing)] + pub id: DocumentableItemId<'db>, + #[serde(skip_serializing)] + pub parent_full_path: Option, + pub name: String, + #[serde(serialize_with = "documentation_serializer")] + pub doc: Option>>, + pub signature: Option, + pub full_path: String, + #[serde(skip_serializing)] + pub code_blocks: Vec, + #[serde(skip_serializing)] + pub doc_location_links: Vec, + pub group: Option, +} + +/// Mimics the [`TopLevelLanguageElementId::full_path`] but skips the macro modules. /// Mimics the [`cairo_lang_defs::ids::TopLevelLanguageElementId::full_path`] but skips the macro modules. /// If not omitted, the path would look like, for example, /// `hello::define_fn_outter!(func_macro_fn_outter);::expose! {\n\t\t\tpub fn func_macro_fn_outter() -> felt252 { \n\t\t\t\tprintln!(\"hello world\");\n\t\t\t\t10 }\n\t\t}::func_macro_fn_outter` From 6045fb90628c45b486ea847be1f1ca4065830cd0 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 26 Nov 2025 20:48:07 +0300 Subject: [PATCH 11/32] lock --- Cargo.lock | 1 + extensions/scarb-doc/Cargo.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 75abe0b17..3c364fdc3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6933,6 +6933,7 @@ dependencies = [ "mimalloc", "salsa", "scarb-build-metadata", + "scarb-execute-utils", "scarb-extensions-cli", "scarb-metadata 1.15.1", "scarb-test-support", diff --git a/extensions/scarb-doc/Cargo.toml b/extensions/scarb-doc/Cargo.toml index c166dc22f..fc1db3465 100644 --- a/extensions/scarb-doc/Cargo.toml +++ b/extensions/scarb-doc/Cargo.toml @@ -31,6 +31,7 @@ scarb-metadata = { path = "../../scarb-metadata" } scarb-build-metadata = { path = "../../utils/scarb-build-metadata" } scarb-ui = { path = "../../utils/scarb-ui" } scarb-extensions-cli = { path = "../../utils/scarb-extensions-cli", default-features = false, features = ["doc"] } +scarb-execute-utils = { path = "../../utils/scarb-execute-utils" } serde.workspace = true serde_json.workspace = true salsa.workspace = true From 5f62bfd781081b29da55f11e097e9a92dbe1a1e7 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 26 Nov 2025 20:55:55 +0300 Subject: [PATCH 12/32] more renames --- .../book.toml | 0 .../src/SUMMARY.md | 0 .../src/hello_world-bar.md | 0 .../src/hello_world-foo.md | 0 .../src/hello_world-foo_bar.md | 0 .../src/hello_world-free_functions.md | 0 .../src/hello_world.md | 0 ...nable_code_blocks.rs => runnable_examples.rs} | 16 ++++++++-------- 8 files changed, 8 insertions(+), 8 deletions(-) rename extensions/scarb-doc/tests/data/{runnable_snippets => runnable_examples}/book.toml (100%) rename extensions/scarb-doc/tests/data/{runnable_snippets => runnable_examples}/src/SUMMARY.md (100%) rename extensions/scarb-doc/tests/data/{runnable_snippets => runnable_examples}/src/hello_world-bar.md (100%) rename extensions/scarb-doc/tests/data/{runnable_snippets => runnable_examples}/src/hello_world-foo.md (100%) rename extensions/scarb-doc/tests/data/{runnable_snippets => runnable_examples}/src/hello_world-foo_bar.md (100%) rename extensions/scarb-doc/tests/data/{runnable_snippets => runnable_examples}/src/hello_world-free_functions.md (100%) rename extensions/scarb-doc/tests/data/{runnable_snippets => runnable_examples}/src/hello_world.md (100%) rename extensions/scarb-doc/tests/{runnable_code_blocks.rs => runnable_examples.rs} (71%) diff --git a/extensions/scarb-doc/tests/data/runnable_snippets/book.toml b/extensions/scarb-doc/tests/data/runnable_examples/book.toml similarity index 100% rename from extensions/scarb-doc/tests/data/runnable_snippets/book.toml rename to extensions/scarb-doc/tests/data/runnable_examples/book.toml diff --git a/extensions/scarb-doc/tests/data/runnable_snippets/src/SUMMARY.md b/extensions/scarb-doc/tests/data/runnable_examples/src/SUMMARY.md similarity index 100% rename from extensions/scarb-doc/tests/data/runnable_snippets/src/SUMMARY.md rename to extensions/scarb-doc/tests/data/runnable_examples/src/SUMMARY.md diff --git a/extensions/scarb-doc/tests/data/runnable_snippets/src/hello_world-bar.md b/extensions/scarb-doc/tests/data/runnable_examples/src/hello_world-bar.md similarity index 100% rename from extensions/scarb-doc/tests/data/runnable_snippets/src/hello_world-bar.md rename to extensions/scarb-doc/tests/data/runnable_examples/src/hello_world-bar.md diff --git a/extensions/scarb-doc/tests/data/runnable_snippets/src/hello_world-foo.md b/extensions/scarb-doc/tests/data/runnable_examples/src/hello_world-foo.md similarity index 100% rename from extensions/scarb-doc/tests/data/runnable_snippets/src/hello_world-foo.md rename to extensions/scarb-doc/tests/data/runnable_examples/src/hello_world-foo.md diff --git a/extensions/scarb-doc/tests/data/runnable_snippets/src/hello_world-foo_bar.md b/extensions/scarb-doc/tests/data/runnable_examples/src/hello_world-foo_bar.md similarity index 100% rename from extensions/scarb-doc/tests/data/runnable_snippets/src/hello_world-foo_bar.md rename to extensions/scarb-doc/tests/data/runnable_examples/src/hello_world-foo_bar.md diff --git a/extensions/scarb-doc/tests/data/runnable_snippets/src/hello_world-free_functions.md b/extensions/scarb-doc/tests/data/runnable_examples/src/hello_world-free_functions.md similarity index 100% rename from extensions/scarb-doc/tests/data/runnable_snippets/src/hello_world-free_functions.md rename to extensions/scarb-doc/tests/data/runnable_examples/src/hello_world-free_functions.md diff --git a/extensions/scarb-doc/tests/data/runnable_snippets/src/hello_world.md b/extensions/scarb-doc/tests/data/runnable_examples/src/hello_world.md similarity index 100% rename from extensions/scarb-doc/tests/data/runnable_snippets/src/hello_world.md rename to extensions/scarb-doc/tests/data/runnable_examples/src/hello_world.md diff --git a/extensions/scarb-doc/tests/runnable_code_blocks.rs b/extensions/scarb-doc/tests/runnable_examples.rs similarity index 71% rename from extensions/scarb-doc/tests/runnable_code_blocks.rs rename to extensions/scarb-doc/tests/runnable_examples.rs index 8470c920a..a8581a437 100644 --- a/extensions/scarb-doc/tests/runnable_code_blocks.rs +++ b/extensions/scarb-doc/tests/runnable_examples.rs @@ -6,15 +6,15 @@ use scarb_test_support::project_builder::ProjectBuilder; mod markdown_target; use markdown_target::MarkdownTargetChecker; -const CODE_WITH_SNIPPETS: &str = include_str!("code/code_12.cairo"); -const EXPECTED_CODE_WITH_SNIPPETS_PATH: &str = "tests/data/runnable_snippets"; +const CODE_WITH_RUNNABLE_CODE_BLOCKS: &str = include_str!("code/code_12.cairo"); +const EXPECTED_WITH_EMBEDDINGS_PATH: &str = "tests/data/runnable_examples"; #[test] fn supports_runnable_examples() { let t = TempDir::new().unwrap(); ProjectBuilder::start() .name("hello_world") - .lib_cairo(CODE_WITH_SNIPPETS) + .lib_cairo(CODE_WITH_RUNNABLE_CODE_BLOCKS) .build(&t); Scarb::quick_command() @@ -25,13 +25,13 @@ fn supports_runnable_examples() { .assert() .success() .stdout_eq(formatdoc! {r#" - [..] Executing snippet #0 from `hello_world::foo_bar` - [..] Compiling hello_world_snippet_0 v0.1.0 ([..]) + [..] Executing example #0 from `hello_world::foo_bar` + [..] Compiling hello_world_example_0 v0.1.0 ([..]) [..] Finished `dev` profile target(s) in [..] - [..] Executing hello_world_snippet_0 + [..] Executing hello_world_example_0 foo bar - Saving output to: target/execute/hello_world_snippet_0/execution1 + Saving output to: target/execute/hello_world_example_0/execution1 Saving output to: target/doc/hello_world Saving build output to: target/doc/hello_world/book @@ -44,6 +44,6 @@ fn supports_runnable_examples() { MarkdownTargetChecker::lenient() .actual(t.path().join("target/doc/hello_world").to_str().unwrap()) - .expected(EXPECTED_CODE_WITH_SNIPPETS_PATH) + .expected(EXPECTED_WITH_EMBEDDINGS_PATH) .assert_all_files_match(); } From 628fba763379301ceae51697c5d296224513556f Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Fri, 28 Nov 2025 15:54:27 +0300 Subject: [PATCH 13/32] initial refactor --- .../scarb-doc/src/docs_generation/markdown.rs | 4 +- .../src/docs_generation/markdown/context.rs | 13 +++++- .../src/docs_generation/markdown/summary.rs | 10 ++--- .../docs_generation/markdown/summary/files.rs | 42 +++--------------- .../markdown/summary/group_files.rs | 19 ++------ .../src/docs_generation/markdown/traits.rs | 44 +++---------------- extensions/scarb-doc/src/main.rs | 39 ++++++++-------- extensions/scarb-doc/src/runner.rs | 27 ++++++------ utils/scarb-extensions-cli/src/doc.rs | 4 ++ 9 files changed, 71 insertions(+), 131 deletions(-) diff --git a/extensions/scarb-doc/src/docs_generation/markdown.rs b/extensions/scarb-doc/src/docs_generation/markdown.rs index abfb9ed71..13fde0400 100644 --- a/extensions/scarb-doc/src/docs_generation/markdown.rs +++ b/extensions/scarb-doc/src/docs_generation/markdown.rs @@ -14,7 +14,7 @@ pub mod traits; use crate::docs_generation::common::{ GeneratedFile, OutputFilesExtension, SummaryIndexMap, SummaryListItem, }; -use crate::runner::ExecutionResult; +use crate::runner::ExecutionResults; use std::ops::Add; const BASE_HEADER_LEVEL: usize = 1; @@ -42,7 +42,7 @@ impl MarkdownContent { pub fn from_crate( package_information: &PackageInformation, format: OutputFilesExtension, - execution_results: Option>, + execution_results: Option, ) -> Result { let (summary, doc_files) = generate_summary_file_content(&package_information.crate_, format, execution_results)?; diff --git a/extensions/scarb-doc/src/docs_generation/markdown/context.rs b/extensions/scarb-doc/src/docs_generation/markdown/context.rs index 2a9fc493d..079d6164f 100644 --- a/extensions/scarb-doc/src/docs_generation/markdown/context.rs +++ b/extensions/scarb-doc/src/docs_generation/markdown/context.rs @@ -8,6 +8,7 @@ use cairo_lang_doc::documentable_item::DocumentableItemId; use cairo_lang_doc::parser::CommentLinkToken; use itertools::Itertools; use std::collections::HashMap; +use crate::runner::ExecutionResults; pub type IncludedItems<'a, 'db> = HashMap, &'a dyn WithPath>; @@ -15,6 +16,7 @@ pub struct MarkdownGenerationContext<'a, 'db> { included_items: IncludedItems<'a, 'db>, formatting: Box, pub(crate) files_extension: &'static str, + execution_results: Option, } pub trait Formatting { @@ -103,7 +105,11 @@ impl Formatting for MarkdownFormatting { } impl<'a, 'db> MarkdownGenerationContext<'a, 'db> { - pub fn from_crate(crate_: &'a Crate<'db>, format: OutputFilesExtension) -> Self + pub fn from_crate( + crate_: &'a Crate<'db>, + format: OutputFilesExtension, + execution_results: Option, + ) -> Self where 'a: 'db, { @@ -118,6 +124,7 @@ impl<'a, 'db> MarkdownGenerationContext<'a, 'db> { included_items, formatting, files_extension: format.get_string(), + execution_results, } } @@ -190,6 +197,10 @@ impl<'a, 'db> MarkdownGenerationContext<'a, 'db> { self.formatting .header_primitive(header_level, name, full_path) } + + pub fn execution_results(&self) -> Option<&ExecutionResults> { + self.execution_results.as_ref() + } } pub fn path_to_file_link(path: &str, files_extension: &str) -> String { diff --git a/extensions/scarb-doc/src/docs_generation/markdown/summary.rs b/extensions/scarb-doc/src/docs_generation/markdown/summary.rs index 473a181f8..1ed6c3e97 100644 --- a/extensions/scarb-doc/src/docs_generation/markdown/summary.rs +++ b/extensions/scarb-doc/src/docs_generation/markdown/summary.rs @@ -13,7 +13,7 @@ use crate::docs_generation::markdown::summary::files::{ }; use crate::docs_generation::markdown::traits::{MarkdownDocItem, TopLevelMarkdownDocItem}; use crate::docs_generation::markdown::{BASE_HEADER_LEVEL, SummaryIndexMap}; -use crate::runner::ExecutionResult; +use crate::runner::ExecutionResults; use crate::types::crate_type::Crate; use anyhow::Result; use group_files::generate_global_groups_summary_files; @@ -21,10 +21,10 @@ use group_files::generate_global_groups_summary_files; pub fn generate_summary_file_content( crate_: &Crate, output_format: OutputFilesExtension, - execution_results: Option>, + execution_results: Option, ) -> Result<(SummaryIndexMap, Vec<(String, String)>)> { let mut summary_index_map = SummaryIndexMap::new(); - let context = MarkdownGenerationContext::from_crate(crate_, output_format); + let context = MarkdownGenerationContext::from_crate(crate_, output_format, execution_results); generate_module_summary_content( &crate_.root_module, @@ -50,7 +50,6 @@ pub fn generate_summary_file_content( BASE_HEADER_LEVEL, None, &summary_index_map, - execution_results.clone(), )?, )]; @@ -58,7 +57,6 @@ pub fn generate_summary_file_content( &crate_.root_module, &context, &summary_index_map, - execution_results.clone(), )?; summary_files.extend(module_item_summaries.to_owned()); @@ -66,7 +64,6 @@ pub fn generate_summary_file_content( &crate_.foreign_crates, &context, &summary_index_map, - execution_results.clone(), )?; summary_files.extend(foreign_modules_files); @@ -75,7 +72,6 @@ pub fn generate_summary_file_content( &crate_.groups, &context, &summary_index_map, - execution_results.clone(), )?; summary_files.extend(groups_files.to_owned()); Ok((summary_index_map, summary_files)) diff --git a/extensions/scarb-doc/src/docs_generation/markdown/summary/files.rs b/extensions/scarb-doc/src/docs_generation/markdown/summary/files.rs index 241e30f89..dab4b8ccc 100644 --- a/extensions/scarb-doc/src/docs_generation/markdown/summary/files.rs +++ b/extensions/scarb-doc/src/docs_generation/markdown/summary/files.rs @@ -6,7 +6,6 @@ use crate::docs_generation::markdown::traits::{ }; use crate::docs_generation::markdown::{BASE_HEADER_LEVEL, BASE_MODULE_CHAPTER_PREFIX}; use crate::docs_generation::{DocItem, TopLevelItems, common}; -use crate::runner::ExecutionResult; use crate::types::module_type::Module; use crate::types::other_types::{ Constant, Enum, ExternFunction, ExternType, FreeFunction, Impl, ImplAlias, MacroDeclaration, @@ -39,7 +38,6 @@ pub fn generate_modules_summary_files( module: &Module, context: &MarkdownGenerationContext, summary_index_map: &SummaryIndexMap, - execution_results: Option>, ) -> Result> { let mut top_level_items = TopLevelItems::default(); let Module { @@ -80,23 +78,14 @@ pub fn generate_modules_summary_files( )?; doc_files.extend::>( - generate_doc_files_for_module_items( - &top_level_items, - context, - summary_index_map, - execution_results.clone(), - )? + generate_doc_files_for_module_items(&top_level_items, context, summary_index_map)? .to_owned(), ); if !top_level_items.modules.is_empty() { for submodule in module.submodules.iter() { - let sub_summaries = &generate_modules_summary_files( - submodule, - context, - summary_index_map, - execution_results.clone(), - )?; + let sub_summaries = + &generate_modules_summary_files(submodule, context, summary_index_map)?; doc_files.extend::>(sub_summaries.to_owned()); } } @@ -107,7 +96,6 @@ pub fn generate_foreign_crates_summary_files( foreign_modules: &Vec, context: &MarkdownGenerationContext, summary_index_map: &SummaryIndexMap, - execution_results: Option>, ) -> Result> { let mut summary_files = vec![]; @@ -119,15 +107,10 @@ pub fn generate_foreign_crates_summary_files( BASE_HEADER_LEVEL, None, summary_index_map, - execution_results.clone(), )?, )]); - let module_item_summaries = &generate_modules_summary_files( - module, - context, - summary_index_map, - execution_results.clone(), - )?; + let module_item_summaries = + &generate_modules_summary_files(module, context, summary_index_map)?; summary_files.extend(module_item_summaries.to_owned()); } Ok(summary_files) @@ -167,80 +150,67 @@ pub fn generate_doc_files_for_module_items( top_level_items: &TopLevelItems, context: &MarkdownGenerationContext, summary_index_map: &SummaryIndexMap, - execution_results: Option>, ) -> Result> { Ok(chain!( generate_top_level_docs_contents( &top_level_items.modules, context, summary_index_map, - execution_results.clone() )?, generate_top_level_docs_contents( &top_level_items.constants, context, summary_index_map, - execution_results.clone() )?, generate_top_level_docs_contents( &top_level_items.free_functions, context, summary_index_map, - execution_results.clone() )?, generate_top_level_docs_contents( &top_level_items.structs, context, summary_index_map, - execution_results.clone() )?, generate_top_level_docs_contents( &top_level_items.enums, context, summary_index_map, - execution_results.clone() )?, generate_top_level_docs_contents( &top_level_items.type_aliases, context, summary_index_map, - execution_results.clone() )?, generate_top_level_docs_contents( &top_level_items.impl_aliases, context, summary_index_map, - execution_results.clone() )?, generate_top_level_docs_contents( &top_level_items.traits, context, summary_index_map, - execution_results.clone() )?, generate_top_level_docs_contents( &top_level_items.impls, context, summary_index_map, - execution_results.clone() )?, generate_top_level_docs_contents( &top_level_items.extern_types, context, summary_index_map, - execution_results.clone() )?, generate_top_level_docs_contents( &top_level_items.extern_functions, context, summary_index_map, - execution_results.clone() )?, generate_top_level_docs_contents( &top_level_items.macro_declarations, context, summary_index_map, - execution_results.clone() )?, ) .collect::>()) @@ -250,7 +220,6 @@ fn generate_top_level_docs_contents( items: &[&impl TopLevelMarkdownDocItem], context: &MarkdownGenerationContext, summary_index_map: &SummaryIndexMap, - execution_results: Option>, ) -> Result> { items .iter() @@ -260,7 +229,6 @@ fn generate_top_level_docs_contents( BASE_HEADER_LEVEL, None, summary_index_map, - execution_results.clone(), ) .map(|markdown| (item.filename(context.files_extension), markdown)) }) diff --git a/extensions/scarb-doc/src/docs_generation/markdown/summary/group_files.rs b/extensions/scarb-doc/src/docs_generation/markdown/summary/group_files.rs index 4daacb476..0b70d35e2 100644 --- a/extensions/scarb-doc/src/docs_generation/markdown/summary/group_files.rs +++ b/extensions/scarb-doc/src/docs_generation/markdown/summary/group_files.rs @@ -8,7 +8,6 @@ use crate::docs_generation::markdown::summary::files::{ use crate::docs_generation::markdown::traits::generate_markdown_table_summary_for_top_level_subitems; use crate::docs_generation::common::SummaryIndexMap; -use crate::runner::ExecutionResult; use crate::types::groups::Group; use itertools::Itertools; @@ -29,7 +28,6 @@ pub fn generate_global_groups_summary_files( groups: &[Group], context: &MarkdownGenerationContext, summary_index_map: &SummaryIndexMap, - execution_results: Option>, ) -> anyhow::Result> { let mut doc_files: Vec<(String, String)> = Vec::new(); @@ -70,13 +68,8 @@ pub fn generate_global_groups_summary_files( )?); doc_files.extend( - generate_doc_files_for_module_items( - &top_level_items, - context, - summary_index_map, - execution_results.clone(), - )? - .to_owned(), + generate_doc_files_for_module_items(&top_level_items, context, summary_index_map)? + .to_owned(), ); doc_files.push(( @@ -86,12 +79,8 @@ pub fn generate_global_groups_summary_files( if !top_level_items.modules.is_empty() { for submodule in group.submodules.iter() { - let sub_summaries = &generate_modules_summary_files( - submodule, - context, - summary_index_map, - execution_results.clone(), - )?; + let sub_summaries = + &generate_modules_summary_files(submodule, context, summary_index_map)?; doc_files.extend::>(sub_summaries.to_owned()); } }; diff --git a/extensions/scarb-doc/src/docs_generation/markdown/traits.rs b/extensions/scarb-doc/src/docs_generation/markdown/traits.rs index f019bd697..edcb8951d 100644 --- a/extensions/scarb-doc/src/docs_generation/markdown/traits.rs +++ b/extensions/scarb-doc/src/docs_generation/markdown/traits.rs @@ -4,7 +4,6 @@ use crate::docs_generation::markdown::{ SHORT_DOCUMENTATION_LEN, }; use crate::docs_generation::{DocItem, PrimitiveDocItem, SubPathDocItem, TopLevelDocItem, common}; -use crate::runner::ExecutionResult; use crate::types::groups::Group; use crate::types::item_data::{ItemData, SubItemData}; use crate::types::module_type::{Module, ModulePubUses}; @@ -84,7 +83,6 @@ macro_rules! impl_markdown_doc_item { header_level: usize, item_suffix: Option, summary_index_map: &SummaryIndexMap, - execution_results: Option>, ) -> Result { let mut markdown = String::new(); @@ -92,7 +90,7 @@ macro_rules! impl_markdown_doc_item { context.get_header_primitive(header_level, self.name(), self.full_path()); writeln!(&mut markdown, "{}\n", header)?; - if let Some(doc) = self.get_documentation(context, execution_results) { + if let Some(doc) = self.get_documentation(context) { writeln!(&mut markdown, "{doc}\n")?; } @@ -138,7 +136,6 @@ pub trait MarkdownDocItem: DocItem { header_level: usize, item_suffix: Option, summary_index_map: &SummaryIndexMap, - execution_results: Option>, ) -> Result; fn get_short_documentation(&self, context: &MarkdownGenerationContext) -> String { @@ -184,11 +181,7 @@ pub trait MarkdownDocItem: DocItem { "—".to_string() } - fn get_documentation( - &self, - context: &MarkdownGenerationContext, - execution_results: Option>, - ) -> Option { + fn get_documentation(&self, context: &MarkdownGenerationContext) -> Option { self.doc().as_ref().map(|doc_tokens| { // TODO: filter out execution results that do not belong to this item doc_tokens @@ -201,12 +194,10 @@ pub trait MarkdownDocItem: DocItem { .code_blocks() .iter() .find(|cb| cb.id.close_token_idx == idx) - && let Some(results) = &execution_results - && let Some(res) = results - .iter() - .find(|exec_res| exec_res.code_block_id == cb.id) + && let Some(results) = context.execution_results() + && let Some(res) = results.get(&cb.id) { - return vec![content.clone(), res.format_as_markdown()]; + return vec![content.clone(), res.as_markdown()]; } vec![content.clone()] } @@ -245,7 +236,6 @@ where header_level: usize, _item_suffix: Option, summary_index_map: &SummaryIndexMap, - execution_results: Option>, ) -> Result { generate_markdown_from_item_data( self, @@ -253,7 +243,6 @@ where header_level, None, summary_index_map, - execution_results, ) } } @@ -265,7 +254,6 @@ impl<'db> MarkdownDocItem for Enum<'db> { header_level: usize, _item_suffix: Option, summary_index_map: &SummaryIndexMap, - execution_results: Option>, ) -> Result { let mut markdown = generate_markdown_from_item_data( self, @@ -273,7 +261,6 @@ impl<'db> MarkdownDocItem for Enum<'db> { header_level, None, summary_index_map, - execution_results.clone(), )?; let mut suffix_calculator = ItemSuffixCalculator::new(self.name()); markdown += &generate_markdown_for_subitems( @@ -282,7 +269,6 @@ impl<'db> MarkdownDocItem for Enum<'db> { header_level, &mut suffix_calculator, summary_index_map, - execution_results.clone(), )?; Ok(markdown) @@ -296,7 +282,6 @@ impl<'db> MarkdownDocItem for Impl<'db> { header_level: usize, _item_suffix: Option, summary_index_map: &SummaryIndexMap, - execution_results: Option>, ) -> Result { let mut markdown = generate_markdown_from_item_data( self, @@ -304,7 +289,6 @@ impl<'db> MarkdownDocItem for Impl<'db> { header_level, None, summary_index_map, - execution_results.clone(), )?; let mut suffix_calculator = ItemSuffixCalculator::new(self.name()); @@ -314,7 +298,6 @@ impl<'db> MarkdownDocItem for Impl<'db> { header_level, &mut suffix_calculator, summary_index_map, - execution_results.clone(), )?; markdown += &generate_markdown_for_subitems( @@ -323,7 +306,6 @@ impl<'db> MarkdownDocItem for Impl<'db> { header_level, &mut suffix_calculator, summary_index_map, - execution_results.clone(), )?; markdown += &generate_markdown_for_subitems( @@ -332,7 +314,6 @@ impl<'db> MarkdownDocItem for Impl<'db> { header_level, &mut suffix_calculator, summary_index_map, - execution_results.clone(), )?; Ok(markdown) @@ -430,7 +411,6 @@ impl<'db> MarkdownDocItem for Module<'db> { header_level: usize, _item_suffix: Option, summary_index_map: &SummaryIndexMap, - execution_results: Option>, ) -> Result { let mut markdown = generate_markdown_from_item_data( self, @@ -438,7 +418,6 @@ impl<'db> MarkdownDocItem for Module<'db> { header_level, None, summary_index_map, - execution_results, )?; markdown += &generate_markdown_table_summary_for_top_level_subitems( @@ -533,7 +512,6 @@ impl<'db> MarkdownDocItem for Struct<'db> { header_level: usize, _item_suffix: Option, summary_index_map: &SummaryIndexMap, - execution_results: Option>, ) -> Result { let mut markdown = generate_markdown_from_item_data( self, @@ -541,7 +519,6 @@ impl<'db> MarkdownDocItem for Struct<'db> { header_level, None, summary_index_map, - execution_results.clone(), )?; let mut suffix_calculator = ItemSuffixCalculator::new(self.name()); @@ -551,7 +528,6 @@ impl<'db> MarkdownDocItem for Struct<'db> { header_level, &mut suffix_calculator, summary_index_map, - execution_results.clone(), )?; Ok(markdown) @@ -565,7 +541,6 @@ impl<'db> MarkdownDocItem for Trait<'db> { header_level: usize, _item_suffix: Option, summary_index_map: &SummaryIndexMap, - execution_results: Option>, ) -> Result { let mut markdown = generate_markdown_from_item_data( self, @@ -573,7 +548,6 @@ impl<'db> MarkdownDocItem for Trait<'db> { header_level, None, summary_index_map, - execution_results.clone(), )?; let mut suffix_calculator = ItemSuffixCalculator::new(self.name()); @@ -583,7 +557,6 @@ impl<'db> MarkdownDocItem for Trait<'db> { header_level, &mut suffix_calculator, summary_index_map, - execution_results.clone(), )?; markdown += &generate_markdown_for_subitems( &self.trait_functions, @@ -591,7 +564,6 @@ impl<'db> MarkdownDocItem for Trait<'db> { header_level, &mut suffix_calculator, summary_index_map, - execution_results.clone(), )?; markdown += &generate_markdown_for_subitems( &self.trait_types, @@ -599,7 +571,6 @@ impl<'db> MarkdownDocItem for Trait<'db> { header_level, &mut suffix_calculator, summary_index_map, - execution_results.clone(), )?; Ok(markdown) } @@ -841,7 +812,6 @@ fn generate_markdown_for_subitems( header_level: usize, suffix_calculator: &mut ItemSuffixCalculator, summary_index_map: &SummaryIndexMap, - execution_results: Option>, ) -> Result { let mut markdown = String::new(); @@ -860,7 +830,6 @@ fn generate_markdown_for_subitems( header_level + 2, postfix, summary_index_map, - execution_results.clone() )? )?; } @@ -875,14 +844,13 @@ fn generate_markdown_from_item_data( header_level: usize, item_suffix: Option, summary_index_map: &SummaryIndexMap, - execution_results: Option>, ) -> Result { let mut markdown = String::new(); let header = context.get_header(header_level, doc_item.name(), doc_item.full_path()); writeln!(&mut markdown, "{}\n", header)?; - if let Some(doc) = doc_item.get_documentation(context, execution_results) { + if let Some(doc) = doc_item.get_documentation(context) { writeln!(&mut markdown, "{doc}\n")?; } diff --git a/extensions/scarb-doc/src/main.rs b/extensions/scarb-doc/src/main.rs index 715b1bec1..219116390 100644 --- a/extensions/scarb-doc/src/main.rs +++ b/extensions/scarb-doc/src/main.rs @@ -7,7 +7,7 @@ use scarb_doc::docs_generation::common::OutputFilesExtension; use scarb_doc::docs_generation::markdown::{MarkdownContent, WorkspaceMarkdownBuilder}; use scarb_doc::errors::{MetadataCommandError, PackagesSerializationError}; use scarb_doc::metadata::get_target_dir; -use scarb_doc::runner::{DocTestRunner, collect_runnable_code_blocks}; +use scarb_doc::runner::{DocTestRunner, ExecutionResults, collect_runnable_code_blocks}; use scarb_doc::versioned_json_output::VersionedJsonOutput; use scarb_doc::{PackageInformation, generate_package_context, generate_package_information}; use scarb_extensions_cli::doc::{Args, OutputFormat}; @@ -38,6 +38,7 @@ fn main_inner(args: Args, ui: Ui) -> Result<()> { let metadata_for_packages = args.packages_filter.match_many(&metadata)?; let output_dir = get_target_dir(&metadata).join(OUTPUT_DIR); let workspace_root = metadata.workspace.root.clone(); + let doc_tests_enabled = !args.no_run; if args.packages_filter.get_workspace() & !matches!(args.output_format, OutputFormat::Json) { let mut builder = WorkspaceMarkdownBuilder::new(args.output_format.into()); @@ -68,13 +69,26 @@ fn main_inner(args: Args, ui: Ui) -> Result<()> { let ctx = generate_package_context(&metadata, pm, args.document_private_items)?; let info = generate_package_information(&ctx, ui.clone())?; print_diagnostics(&ui); - output.write(info)?; + let execution_results = doc_tests_enabled + .then(|| run_doc_tests(&info, &ui)) + .transpose()?; + output.write(info, execution_results)?; } output.flush()?; } Ok(()) } +fn run_doc_tests(package: &PackageInformation, ui: &Ui) -> Result { + let runnable_code_blocks = collect_runnable_code_blocks(&package.crate_); + if runnable_code_blocks.is_empty() { + Ok(Default::default()) + } else { + let runner = DocTestRunner::new(&package.package_metadata, ui.clone()); + runner.execute(&runnable_code_blocks) + } +} + pub enum OutputEmit { Markdown { output_dir: Utf8PathBuf, @@ -126,7 +140,11 @@ impl OutputEmit { } } - pub fn write(&mut self, package: PackageInformation) -> Result<()> { + pub fn write( + &mut self, + package: PackageInformation, + execution_results: Option, + ) -> Result<()> { match self { OutputEmit::Markdown { output_dir, @@ -135,21 +153,6 @@ impl OutputEmit { ui, files_extension, } => { - // TODO: refactor this - let execution_enabled = *build; - let execution_results = if execution_enabled { - let runnable_code_blocks = collect_runnable_code_blocks(&package.crate_); - if !runnable_code_blocks.is_empty() { - let runner = DocTestRunner::new(&package.package_metadata, ui.clone()); - let execution_results = runner.execute(&runnable_code_blocks)?; - Some(execution_results) - } else { - Some(vec![]) - } - } else { - None - }; - let content = MarkdownContent::from_crate(&package, *files_extension, execution_results)?; output_markdown( diff --git a/extensions/scarb-doc/src/runner.rs b/extensions/scarb-doc/src/runner.rs index 267f6520e..358bd23d7 100644 --- a/extensions/scarb-doc/src/runner.rs +++ b/extensions/scarb-doc/src/runner.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use crate::code_blocks::{CodeBlock, CodeBlockId}; use crate::types::crate_type::Crate; use crate::types::module_type::Module; @@ -17,16 +18,17 @@ use scarb_ui::components::Status; use std::fs; use tempfile::tempdir; +pub type ExecutionResults = HashMap; + #[derive(Debug, Clone)] -pub struct ExecutionResult { - pub code_block_id: CodeBlockId, +pub struct ExecutionOutput { pub print_output: String, pub program_output: String, } -impl ExecutionResult { +impl ExecutionOutput { /// Formats the execution result as markdown with code blocks. - pub fn format_as_markdown(&self) -> String { + pub fn as_markdown(&self) -> String { let mut output = String::new(); if !self.print_output.is_empty() { output.push_str("\nOutput:\n```\n"); @@ -60,16 +62,16 @@ impl<'a> DocTestRunner<'a> { } } - pub fn execute(&self, code_blocks: &[CodeBlock]) -> Result> { - let mut results = Vec::new(); + pub fn execute(&self, code_blocks: &[CodeBlock]) -> Result { + let mut results = HashMap::new(); for (index, code_block) in code_blocks.iter().enumerate() { let result = self.execute_single(code_block, index)?; - results.push(result); + results.insert(code_block.id.clone(), result); } Ok(results) } - fn execute_single(&self, code_block: &CodeBlock, index: usize) -> Result { + fn execute_single(&self, code_block: &CodeBlock, index: usize) -> Result { let temp_dir = tempdir().context("failed to create temporary workspace for doc snippet execution")?; let project_dir = Utf8PathBuf::from_path_buf(temp_dir.path().to_path_buf()) @@ -80,8 +82,7 @@ impl<'a> DocTestRunner<'a> { let (print_output, program_output) = self.run_execute(&project_dir, index, code_block)?; - Ok(ExecutionResult { - code_block_id: code_block.id.clone(), + Ok(ExecutionOutput { print_output, program_output, }) @@ -115,7 +116,8 @@ impl<'a> DocTestRunner<'a> { [executable] "#}; - fs::write(dir.join("Scarb.toml"), manifest).context("failed to write manifest for example")?; + fs::write(dir.join("Scarb.toml"), manifest) + .context("failed to write manifest for example")?; Ok(()) } @@ -143,8 +145,7 @@ impl<'a> DocTestRunner<'a> { {body} }} "# }; - fs::write(src_dir.join("lib.cairo"), lib_cairo) - .context("failed to write lib.cairo")?; + fs::write(src_dir.join("lib.cairo"), lib_cairo).context("failed to write lib.cairo")?; Ok(()) } diff --git a/utils/scarb-extensions-cli/src/doc.rs b/utils/scarb-extensions-cli/src/doc.rs index 9958845b6..0beecd8c2 100644 --- a/utils/scarb-extensions-cli/src/doc.rs +++ b/utils/scarb-extensions-cli/src/doc.rs @@ -45,6 +45,10 @@ pub struct Args { #[arg(long, default_value_t = false)] pub build: bool, + /// Do not run doc tests. + #[arg(long, default_value_t = false)] + pub no_run: bool, + /// Specifies features to enable. #[command(flatten)] pub features: FeaturesSpec, From 502c3bfba151b80cc29c1b4e267076073e8d69b0 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Mon, 1 Dec 2025 05:54:57 +0300 Subject: [PATCH 14/32] further refactor --- extensions/scarb-doc/src/code_blocks.rs | 32 ++++++++-- .../src/docs_generation/markdown/context.rs | 2 +- .../src/docs_generation/markdown/summary.rs | 14 ++--- .../docs_generation/markdown/summary/files.rs | 54 ++++------------- .../src/docs_generation/markdown/traits.rs | 60 ++++--------------- extensions/scarb-doc/src/runner.rs | 45 +++++++++++--- 6 files changed, 92 insertions(+), 115 deletions(-) diff --git a/extensions/scarb-doc/src/code_blocks.rs b/extensions/scarb-doc/src/code_blocks.rs index 82110024e..982734cfa 100644 --- a/extensions/scarb-doc/src/code_blocks.rs +++ b/extensions/scarb-doc/src/code_blocks.rs @@ -1,3 +1,4 @@ +use crate::runner::{ExecutionOutcome, RunStrategy}; use cairo_lang_doc::parser::DocumentationCommentToken; use std::str::from_utf8; @@ -22,6 +23,8 @@ pub enum CodeBlockAttribute { Runnable, Ignore, NoRun, + CompileFail, + ShouldPanic, Other(String), } @@ -32,6 +35,8 @@ impl From<&str> for CodeBlockAttribute { "runnable" => CodeBlockAttribute::Runnable, "ignore" => CodeBlockAttribute::Ignore, "no_run" | "no-run" => CodeBlockAttribute::NoRun, + "should_panic" | "should-panic" => CodeBlockAttribute::ShouldPanic, + "compile_fail" | "compile-fail" => CodeBlockAttribute::CompileFail, _ => CodeBlockAttribute::Other(string.to_string()), } } @@ -65,17 +70,34 @@ impl CodeBlock { false } - // TODO: consider runnable by default unless specified otherwise? - pub fn should_run(&self) -> bool { - self.is_cairo() && self.attributes.contains(&CodeBlockAttribute::Runnable) - // && !self.attributes.contains(&CodeBlockAttribute::Ignore) - // && !self.attributes.contains(&CodeBlockAttribute::NoRun) + pub fn run_strategy(&self) -> RunStrategy { + if self.attributes.contains(&CodeBlockAttribute::Ignore) { + return RunStrategy::Ignore; + } + // TODO: remove this later on + if !self.is_cairo() { + return RunStrategy::Ignore; + } + if self.attributes.contains(&CodeBlockAttribute::NoRun) { + return RunStrategy::Build; + } + if self.attributes.contains(&CodeBlockAttribute::Runnable) { + return RunStrategy::Run; + } + // TODO: Default to run unless specified otherwise + RunStrategy::Ignore } // TODO: implement building examples without running them #[allow(unused)] pub fn should_build(&self) -> bool { self.is_cairo() && !self.attributes.contains(&CodeBlockAttribute::Ignore) + /// Returns the expected execution outcome based on attributes. + pub fn expected_outcome(&self) -> ExecutionOutcome { + if self.attributes.contains(&CodeBlockAttribute::Ignore) { + return ExecutionOutcome::None; + } + ExecutionOutcome::Success } fn parse_attributes(info_string: &str) -> Vec { diff --git a/extensions/scarb-doc/src/docs_generation/markdown/context.rs b/extensions/scarb-doc/src/docs_generation/markdown/context.rs index 079d6164f..009d52ed3 100644 --- a/extensions/scarb-doc/src/docs_generation/markdown/context.rs +++ b/extensions/scarb-doc/src/docs_generation/markdown/context.rs @@ -2,13 +2,13 @@ use crate::docs_generation::common::{OutputFilesExtension, SummaryIndexMap}; use crate::docs_generation::markdown::SUMMARY_FILENAME; use crate::docs_generation::markdown::traits::WithPath; use crate::location_links::DocLocationLink; +use crate::runner::ExecutionResults; use crate::types::crate_type::Crate; use cairo_lang_defs::ids::{ImplItemId, LookupItemId, TraitItemId}; use cairo_lang_doc::documentable_item::DocumentableItemId; use cairo_lang_doc::parser::CommentLinkToken; use itertools::Itertools; use std::collections::HashMap; -use crate::runner::ExecutionResults; pub type IncludedItems<'a, 'db> = HashMap, &'a dyn WithPath>; diff --git a/extensions/scarb-doc/src/docs_generation/markdown/summary.rs b/extensions/scarb-doc/src/docs_generation/markdown/summary.rs index 1ed6c3e97..80440479c 100644 --- a/extensions/scarb-doc/src/docs_generation/markdown/summary.rs +++ b/extensions/scarb-doc/src/docs_generation/markdown/summary.rs @@ -53,11 +53,8 @@ pub fn generate_summary_file_content( )?, )]; - let module_item_summaries = &generate_modules_summary_files( - &crate_.root_module, - &context, - &summary_index_map, - )?; + let module_item_summaries = + &generate_modules_summary_files(&crate_.root_module, &context, &summary_index_map)?; summary_files.extend(module_item_summaries.to_owned()); let foreign_modules_files = generate_foreign_crates_summary_files( @@ -68,11 +65,8 @@ pub fn generate_summary_file_content( summary_files.extend(foreign_modules_files); - let groups_files = generate_global_groups_summary_files( - &crate_.groups, - &context, - &summary_index_map, - )?; + let groups_files = + generate_global_groups_summary_files(&crate_.groups, &context, &summary_index_map)?; summary_files.extend(groups_files.to_owned()); Ok((summary_index_map, summary_files)) } diff --git a/extensions/scarb-doc/src/docs_generation/markdown/summary/files.rs b/extensions/scarb-doc/src/docs_generation/markdown/summary/files.rs index dab4b8ccc..103564764 100644 --- a/extensions/scarb-doc/src/docs_generation/markdown/summary/files.rs +++ b/extensions/scarb-doc/src/docs_generation/markdown/summary/files.rs @@ -79,7 +79,7 @@ pub fn generate_modules_summary_files( doc_files.extend::>( generate_doc_files_for_module_items(&top_level_items, context, summary_index_map)? - .to_owned(), + .to_owned(), ); if !top_level_items.modules.is_empty() { @@ -102,12 +102,7 @@ pub fn generate_foreign_crates_summary_files( for module in foreign_modules { summary_files.extend(vec![( module.filename(context.files_extension), - module.generate_markdown( - context, - BASE_HEADER_LEVEL, - None, - summary_index_map, - )?, + module.generate_markdown(context, BASE_HEADER_LEVEL, None, summary_index_map)?, )]); let module_item_summaries = &generate_modules_summary_files(module, context, summary_index_map)?; @@ -152,31 +147,15 @@ pub fn generate_doc_files_for_module_items( summary_index_map: &SummaryIndexMap, ) -> Result> { Ok(chain!( - generate_top_level_docs_contents( - &top_level_items.modules, - context, - summary_index_map, - )?, - generate_top_level_docs_contents( - &top_level_items.constants, - context, - summary_index_map, - )?, + generate_top_level_docs_contents(&top_level_items.modules, context, summary_index_map,)?, + generate_top_level_docs_contents(&top_level_items.constants, context, summary_index_map,)?, generate_top_level_docs_contents( &top_level_items.free_functions, context, summary_index_map, )?, - generate_top_level_docs_contents( - &top_level_items.structs, - context, - summary_index_map, - )?, - generate_top_level_docs_contents( - &top_level_items.enums, - context, - summary_index_map, - )?, + generate_top_level_docs_contents(&top_level_items.structs, context, summary_index_map,)?, + generate_top_level_docs_contents(&top_level_items.enums, context, summary_index_map,)?, generate_top_level_docs_contents( &top_level_items.type_aliases, context, @@ -187,16 +166,8 @@ pub fn generate_doc_files_for_module_items( context, summary_index_map, )?, - generate_top_level_docs_contents( - &top_level_items.traits, - context, - summary_index_map, - )?, - generate_top_level_docs_contents( - &top_level_items.impls, - context, - summary_index_map, - )?, + generate_top_level_docs_contents(&top_level_items.traits, context, summary_index_map,)?, + generate_top_level_docs_contents(&top_level_items.impls, context, summary_index_map,)?, generate_top_level_docs_contents( &top_level_items.extern_types, context, @@ -224,13 +195,8 @@ fn generate_top_level_docs_contents( items .iter() .map(|item| { - item.generate_markdown( - context, - BASE_HEADER_LEVEL, - None, - summary_index_map, - ) - .map(|markdown| (item.filename(context.files_extension), markdown)) + item.generate_markdown(context, BASE_HEADER_LEVEL, None, summary_index_map) + .map(|markdown| (item.filename(context.files_extension), markdown)) }) .collect() } diff --git a/extensions/scarb-doc/src/docs_generation/markdown/traits.rs b/extensions/scarb-doc/src/docs_generation/markdown/traits.rs index edcb8951d..302c05114 100644 --- a/extensions/scarb-doc/src/docs_generation/markdown/traits.rs +++ b/extensions/scarb-doc/src/docs_generation/markdown/traits.rs @@ -237,13 +237,7 @@ where _item_suffix: Option, summary_index_map: &SummaryIndexMap, ) -> Result { - generate_markdown_from_item_data( - self, - context, - header_level, - None, - summary_index_map, - ) + generate_markdown_from_item_data(self, context, header_level, None, summary_index_map) } } @@ -255,13 +249,8 @@ impl<'db> MarkdownDocItem for Enum<'db> { _item_suffix: Option, summary_index_map: &SummaryIndexMap, ) -> Result { - let mut markdown = generate_markdown_from_item_data( - self, - context, - header_level, - None, - summary_index_map, - )?; + let mut markdown = + generate_markdown_from_item_data(self, context, header_level, None, summary_index_map)?; let mut suffix_calculator = ItemSuffixCalculator::new(self.name()); markdown += &generate_markdown_for_subitems( &self.variants, @@ -283,13 +272,8 @@ impl<'db> MarkdownDocItem for Impl<'db> { _item_suffix: Option, summary_index_map: &SummaryIndexMap, ) -> Result { - let mut markdown = generate_markdown_from_item_data( - self, - context, - header_level, - None, - summary_index_map, - )?; + let mut markdown = + generate_markdown_from_item_data(self, context, header_level, None, summary_index_map)?; let mut suffix_calculator = ItemSuffixCalculator::new(self.name()); markdown += &generate_markdown_for_subitems( @@ -412,13 +396,8 @@ impl<'db> MarkdownDocItem for Module<'db> { _item_suffix: Option, summary_index_map: &SummaryIndexMap, ) -> Result { - let mut markdown = generate_markdown_from_item_data( - self, - context, - header_level, - None, - summary_index_map, - )?; + let mut markdown = + generate_markdown_from_item_data(self, context, header_level, None, summary_index_map)?; markdown += &generate_markdown_table_summary_for_top_level_subitems( &self.submodules.iter().collect_vec(), @@ -513,13 +492,8 @@ impl<'db> MarkdownDocItem for Struct<'db> { _item_suffix: Option, summary_index_map: &SummaryIndexMap, ) -> Result { - let mut markdown = generate_markdown_from_item_data( - self, - context, - header_level, - None, - summary_index_map, - )?; + let mut markdown = + generate_markdown_from_item_data(self, context, header_level, None, summary_index_map)?; let mut suffix_calculator = ItemSuffixCalculator::new(self.name()); markdown += &generate_markdown_for_subitems( @@ -542,13 +516,8 @@ impl<'db> MarkdownDocItem for Trait<'db> { _item_suffix: Option, summary_index_map: &SummaryIndexMap, ) -> Result { - let mut markdown = generate_markdown_from_item_data( - self, - context, - header_level, - None, - summary_index_map, - )?; + let mut markdown = + generate_markdown_from_item_data(self, context, header_level, None, summary_index_map)?; let mut suffix_calculator = ItemSuffixCalculator::new(self.name()); markdown += &generate_markdown_for_subitems( @@ -825,12 +794,7 @@ fn generate_markdown_for_subitems( writeln!( &mut markdown, "{}", - item.generate_markdown( - context, - header_level + 2, - postfix, - summary_index_map, - )? + item.generate_markdown(context, header_level + 2, postfix, summary_index_map,)? )?; } } diff --git a/extensions/scarb-doc/src/runner.rs b/extensions/scarb-doc/src/runner.rs index 358bd23d7..15a4327ec 100644 --- a/extensions/scarb-doc/src/runner.rs +++ b/extensions/scarb-doc/src/runner.rs @@ -1,4 +1,3 @@ -use std::collections::HashMap; use crate::code_blocks::{CodeBlock, CodeBlockId}; use crate::types::crate_type::Crate; use crate::types::module_type::Module; @@ -15,18 +14,20 @@ use scarb_execute_utils::{ use scarb_metadata::{PackageMetadata, ScarbCommand}; use scarb_ui::Ui; use scarb_ui::components::Status; +use std::collections::HashMap; use std::fs; use tempfile::tempdir; -pub type ExecutionResults = HashMap; +pub type ExecutionResults = HashMap; #[derive(Debug, Clone)] -pub struct ExecutionOutput { +pub struct ExecutionResult { + pub outcome: ExecutionOutcome, pub print_output: String, pub program_output: String, } -impl ExecutionOutput { +impl ExecutionResult { /// Formats the execution result as markdown with code blocks. pub fn as_markdown(&self) -> String { let mut output = String::new(); @@ -47,6 +48,34 @@ impl ExecutionOutput { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExecutionOutcome { + Success, + CompileError, + RuntimeError, + None, +} + +#[derive(Debug, Clone, Default)] +pub struct ExecutionSummary { + pub passed: usize, + pub failed: usize, + pub ignored: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TestStatus { + Passed, + Failed, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RunStrategy { + Ignore, + Build, + Run, +} + /// A runner for executing examples (code blocks) found in documentation. /// Uses `scarb execute` and runs code blocks in isolated temporary workspaces. pub struct DocTestRunner<'a> { @@ -71,7 +100,7 @@ impl<'a> DocTestRunner<'a> { Ok(results) } - fn execute_single(&self, code_block: &CodeBlock, index: usize) -> Result { + fn execute_single(&self, code_block: &CodeBlock, index: usize) -> Result { let temp_dir = tempdir().context("failed to create temporary workspace for doc snippet execution")?; let project_dir = Utf8PathBuf::from_path_buf(temp_dir.path().to_path_buf()) @@ -82,9 +111,10 @@ impl<'a> DocTestRunner<'a> { let (print_output, program_output) = self.run_execute(&project_dir, index, code_block)?; - Ok(ExecutionOutput { + Ok(ExecutionResult { print_output, program_output, + outcome: ExecutionOutcome::Success, }) } @@ -179,6 +209,7 @@ impl<'a> DocTestRunner<'a> { "SCARB_MANIFEST_PATH", project_dir.join("Scarb.toml").as_str(), ) + .env("SCARB_ALL_FEATURES", "true") .run() .with_context(|| "execution failed")?; @@ -230,7 +261,7 @@ fn collect_from_module(module: &Module<'_>, runnable_code_blocks: &mut Vec, runnable_code_blocks: &mut Vec) { for block in &item_data.code_blocks { - if block.should_run() { + if block.run_strategy() == RunStrategy::Run { runnable_code_blocks.push(block.clone()); } } From d436690d17b4a71a7ccc0b0d9595c0304ad253d4 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Mon, 1 Dec 2025 12:34:43 +0300 Subject: [PATCH 15/32] sort doctests to run deterministically --- extensions/scarb-doc/src/code_blocks.rs | 2 +- extensions/scarb-doc/src/runner.rs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/extensions/scarb-doc/src/code_blocks.rs b/extensions/scarb-doc/src/code_blocks.rs index 982734cfa..83b9cf9e2 100644 --- a/extensions/scarb-doc/src/code_blocks.rs +++ b/extensions/scarb-doc/src/code_blocks.rs @@ -2,7 +2,7 @@ use crate::runner::{ExecutionOutcome, RunStrategy}; use cairo_lang_doc::parser::DocumentationCommentToken; use std::str::from_utf8; -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] pub struct CodeBlockId { pub item_full_path: String, pub close_token_idx: usize, diff --git a/extensions/scarb-doc/src/runner.rs b/extensions/scarb-doc/src/runner.rs index 15a4327ec..12080110b 100644 --- a/extensions/scarb-doc/src/runner.rs +++ b/extensions/scarb-doc/src/runner.rs @@ -247,6 +247,8 @@ pub fn collect_runnable_code_blocks(crate_: &Crate<'_>) -> Vec { for module in &crate_.foreign_crates { collect_from_module(module, &mut runnable_code_blocks); } + // Sort to run deterministically + runnable_code_blocks.sort_by_key(|block| block.id.clone()); runnable_code_blocks } From 9d131016e18d4d23fc89ae9bdbd101a84d42cd1b Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Mon, 1 Dec 2025 13:42:29 +0300 Subject: [PATCH 16/32] update `CodeBlock` methods --- extensions/scarb-doc/src/code_blocks.rs | 32 ++++++++++++++----------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/extensions/scarb-doc/src/code_blocks.rs b/extensions/scarb-doc/src/code_blocks.rs index 83b9cf9e2..302133e21 100644 --- a/extensions/scarb-doc/src/code_blocks.rs +++ b/extensions/scarb-doc/src/code_blocks.rs @@ -65,7 +65,7 @@ impl CodeBlock { if self.attributes.contains(&CodeBlockAttribute::Cairo) { return true; } - // Assume unknown attributes imply non-Cairo code. + // TODO: Assume unknown attributes imply non-Cairo code. // !self.attributes.iter().any(|attr| matches!(attr, CodeBlockAttribute::Other(_))) false } @@ -75,29 +75,33 @@ impl CodeBlock { return RunStrategy::Ignore; } // TODO: remove this later on - if !self.is_cairo() { + if !self.is_cairo() || !self.attributes.contains(&CodeBlockAttribute::Runnable) { return RunStrategy::Ignore; } - if self.attributes.contains(&CodeBlockAttribute::NoRun) { - return RunStrategy::Build; - } - if self.attributes.contains(&CodeBlockAttribute::Runnable) { - return RunStrategy::Run; + match self.expected_outcome() { + ExecutionOutcome::None => RunStrategy::Ignore, + ExecutionOutcome::BuildSuccess => RunStrategy::Build, + ExecutionOutcome::RunSuccess => RunStrategy::Execute, + ExecutionOutcome::CompileError => RunStrategy::Build, + ExecutionOutcome::RuntimeError => RunStrategy::Execute, } - // TODO: Default to run unless specified otherwise - RunStrategy::Ignore } - // TODO: implement building examples without running them - #[allow(unused)] - pub fn should_build(&self) -> bool { - self.is_cairo() && !self.attributes.contains(&CodeBlockAttribute::Ignore) /// Returns the expected execution outcome based on attributes. pub fn expected_outcome(&self) -> ExecutionOutcome { if self.attributes.contains(&CodeBlockAttribute::Ignore) { return ExecutionOutcome::None; } - ExecutionOutcome::Success + if self.attributes.contains(&CodeBlockAttribute::CompileFail) { + return ExecutionOutcome::CompileError; + } + if self.attributes.contains(&CodeBlockAttribute::ShouldPanic) { + return ExecutionOutcome::RuntimeError; + } + if self.attributes.contains(&CodeBlockAttribute::NoRun) { + return ExecutionOutcome::BuildSuccess; + } + ExecutionOutcome::RunSuccess } fn parse_attributes(info_string: &str) -> Vec { From 6595f42c2e98a68464cc17e3b4bffa653ba381c7 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Mon, 1 Dec 2025 14:02:13 +0300 Subject: [PATCH 17/32] further refactors --- extensions/scarb-doc/src/main.rs | 9 +- extensions/scarb-doc/src/runner.rs | 329 +++++++++++------- .../scarb-doc/tests/runnable_examples.rs | 5 +- 3 files changed, 215 insertions(+), 128 deletions(-) diff --git a/extensions/scarb-doc/src/main.rs b/extensions/scarb-doc/src/main.rs index 219116390..ac8b919b0 100644 --- a/extensions/scarb-doc/src/main.rs +++ b/extensions/scarb-doc/src/main.rs @@ -7,7 +7,7 @@ use scarb_doc::docs_generation::common::OutputFilesExtension; use scarb_doc::docs_generation::markdown::{MarkdownContent, WorkspaceMarkdownBuilder}; use scarb_doc::errors::{MetadataCommandError, PackagesSerializationError}; use scarb_doc::metadata::get_target_dir; -use scarb_doc::runner::{DocTestRunner, ExecutionResults, collect_runnable_code_blocks}; +use scarb_doc::runner::{ExecutionResults, TestRunner, collect_code_blocks}; use scarb_doc::versioned_json_output::VersionedJsonOutput; use scarb_doc::{PackageInformation, generate_package_context, generate_package_information}; use scarb_extensions_cli::doc::{Args, OutputFormat}; @@ -80,12 +80,13 @@ fn main_inner(args: Args, ui: Ui) -> Result<()> { } fn run_doc_tests(package: &PackageInformation, ui: &Ui) -> Result { - let runnable_code_blocks = collect_runnable_code_blocks(&package.crate_); + let runnable_code_blocks = collect_code_blocks(&package.crate_); if runnable_code_blocks.is_empty() { Ok(Default::default()) } else { - let runner = DocTestRunner::new(&package.package_metadata, ui.clone()); - runner.execute(&runnable_code_blocks) + let runner = TestRunner::new(&package.package_metadata, ui.clone()); + let (_summary, results) = runner.execute(&runnable_code_blocks)?; + Ok(results) } } diff --git a/extensions/scarb-doc/src/runner.rs b/extensions/scarb-doc/src/runner.rs index 12080110b..e2708cc2f 100644 --- a/extensions/scarb-doc/src/runner.rs +++ b/extensions/scarb-doc/src/runner.rs @@ -3,6 +3,7 @@ use crate::types::crate_type::Crate; use crate::types::module_type::Module; use crate::types::other_types::ItemData; use anyhow::{Context, Result, anyhow}; +use cairo_lang_filesystem::db::Edition; use camino::{Utf8Path, Utf8PathBuf}; use create_output_dir::create_output_dir; use indoc::formatdoc; @@ -16,15 +17,17 @@ use scarb_ui::Ui; use scarb_ui::components::Status; use std::collections::HashMap; use std::fs; -use tempfile::tempdir; +use std::fmt::Write; +use tempfile::{TempDir, tempdir}; pub type ExecutionResults = HashMap; #[derive(Debug, Clone)] pub struct ExecutionResult { - pub outcome: ExecutionOutcome, + pub status: TestStatus, pub print_output: String, pub program_output: String, + pub outcome: ExecutionOutcome, } impl ExecutionResult { @@ -41,21 +44,13 @@ impl ExecutionResult { output.push_str(&self.program_output); output.push_str("\n```\n"); } - if output.is_empty() { + if output.is_empty() && self.outcome == ExecutionOutcome::RunSuccess { output.push_str("\n*No output.*\n"); } output } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ExecutionOutcome { - Success, - CompileError, - RuntimeError, - None, -} - #[derive(Debug, Clone, Default)] pub struct ExecutionSummary { pub passed: usize, @@ -69,21 +64,30 @@ pub enum TestStatus { Failed, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExecutionOutcome { + BuildSuccess, + RunSuccess, + CompileError, + RuntimeError, + None, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RunStrategy { Ignore, Build, - Run, + Execute, } /// A runner for executing examples (code blocks) found in documentation. /// Uses `scarb execute` and runs code blocks in isolated temporary workspaces. -pub struct DocTestRunner<'a> { +pub struct TestRunner<'a> { package_metadata: &'a PackageMetadata, ui: Ui, } -impl<'a> DocTestRunner<'a> { +impl<'a> TestRunner<'a> { pub fn new(package_metadata: &'a PackageMetadata, ui: Ui) -> Self { Self { package_metadata, @@ -91,82 +95,216 @@ impl<'a> DocTestRunner<'a> { } } - pub fn execute(&self, code_blocks: &[CodeBlock]) -> Result { + pub fn execute( + &self, + code_blocks: &[CodeBlock], + ) -> Result<(ExecutionSummary, ExecutionResults)> { let mut results = HashMap::new(); - for (index, code_block) in code_blocks.iter().enumerate() { - let result = self.execute_single(code_block, index)?; - results.insert(code_block.id.clone(), result); - } - Ok(results) - } + let mut summary = ExecutionSummary::default(); + let (to_run, ignored): (Vec<_>, Vec<_>) = code_blocks + .iter() + .partition(|block| block.run_strategy() != RunStrategy::Ignore); - fn execute_single(&self, code_block: &CodeBlock, index: usize) -> Result { - let temp_dir = - tempdir().context("failed to create temporary workspace for doc snippet execution")?; - let project_dir = Utf8PathBuf::from_path_buf(temp_dir.path().to_path_buf()) - .map_err(|path| anyhow!("path `{}` is not UTF-8 encoded", path.display()))?; + self.ui.print(Status::new( + "Found", + &format!("{} doc tests; {} ignored", code_blocks.len(), ignored.len(),), + )); + self.ui.print(Status::new( + "Running", + &format!("{} doc tests", to_run.len()), + )); + for (index, code_block) in to_run.iter().enumerate() { + let strategy = code_block.run_strategy(); + match self.execute_single(code_block, index, strategy) { + Ok(res) => { + if res.status == TestStatus::Passed { + summary.passed += 1; + results.insert(code_block.id.clone(), res); + } else { + summary.failed += 1; + } + } + Err(e) => { + summary.failed += 1; + self.ui.error(format!("Error running example #{}: {:#}", index, e)); + } + } + } - self.write_manifest(&project_dir, index)?; - self.write_lib_cairo(&project_dir, code_block)?; + Ok((summary, results)) + } - let (print_output, program_output) = self.run_execute(&project_dir, index, code_block)?; + fn execute_single( + &self, + code_block: &CodeBlock, + index: usize, + strategy: RunStrategy, + ) -> Result { + let ws = TestWorkspace::new(self.package_metadata, index, code_block)?; + self.ui.print(Status::new( + "Running", + format!("example #{} from `{}`", ws.index, ws.item_full_path).as_str(), + )); + let (actual, print_output, program_output) = self.run_test(&ws, strategy)?; + let expected = code_block.expected_outcome(); + let status = if actual == expected { + TestStatus::Passed + } else { + TestStatus::Failed + }; + match status { + TestStatus::Passed => self.ui.print(Status::new( + "Passed", + &format!("example #{} from `{}`", ws.index, ws.item_full_path), + )), + TestStatus::Failed => self.ui.print(Status::new( + "Failed", + &format!("example #{} from `{}`", ws.index, ws.item_full_path), + )), + } Ok(ExecutionResult { + status, print_output, program_output, - outcome: ExecutionOutcome::Success, + outcome: actual, }) } - // TODO: consider using `ProjectBuilder` instead. - // - multiple snippets per package - // - or multiple packages with snippets in a workspace with common dep - fn write_manifest(&self, dir: &Utf8Path, index: usize) -> Result<()> { - let package_name = &self.package_metadata.name; - let package_dir = self - .package_metadata + fn run_test( + &self, + ws: &TestWorkspace, + strategy: RunStrategy, + ) -> Result<(ExecutionOutcome, String, String)> { + if strategy == RunStrategy::Ignore { + unreachable!("the code block should be filtered out before reaching here"); + } + let target_dir = ws.root().join("target"); + let build_result = ScarbCommand::new() + .arg("build") + .current_dir(ws.root()) + .env("SCARB_TARGET_DIR", target_dir.as_str()) + .env("SCARB_UI_VERBOSITY", self.ui.verbosity().to_string()) + .env("SCARB_MANIFEST_PATH", ws.manifest_path().as_str()) + .env("SCARB_ALL_FEATURES", "true") + .run(); + if build_result.is_err() { + return Ok((ExecutionOutcome::CompileError, String::new(), String::new())); + } + if strategy == RunStrategy::Build { + return Ok((ExecutionOutcome::RunSuccess, String::new(), String::new())); + } + let output_dir = target_dir.join("execute").join(&ws.package_name); + create_output_dir(output_dir.as_std_path())?; + let (output_dir, execution_id) = incremental_create_execution_output_dir(&output_dir)?; + + let run_result = ScarbCommand::new() + .arg("execute") + .arg("--no-build") + .arg("--save-print-output") + .arg("--save-program-output") + .current_dir(ws.root()) + .env("SCARB_EXECUTION_ID", execution_id.to_string()) + .env("SCARB_TARGET_DIR", target_dir.as_str()) + .env("SCARB_UI_VERBOSITY", self.ui.verbosity().to_string()) + .env("SCARB_MANIFEST_PATH", ws.manifest_path().as_str()) + .env("SCARB_ALL_FEATURES", "true") + .run(); + + if run_result.is_err() { + return Ok((ExecutionOutcome::RuntimeError, String::new(), String::new())); + } + let print_output = fs::read_to_string(output_dir.join(EXECUTE_PRINT_OUTPUT_FILENAME)) + .unwrap_or_default() + .trim() + .to_string(); + let program_output = fs::read_to_string(output_dir.join(EXECUTE_PROGRAM_OUTPUT_FILENAME)) + .unwrap_or_default() + .trim() + .to_string(); + Ok((ExecutionOutcome::RunSuccess, print_output, program_output)) + } +} + +struct TestWorkspace { + _temp_dir: TempDir, + root: Utf8PathBuf, + manifest_path: Utf8PathBuf, + package_name: String, + index: usize, + item_full_path: String, +} + +impl TestWorkspace { + fn new(metadata: &PackageMetadata, index: usize, code_block: &CodeBlock) -> Result { + let temp_dir = tempdir().context("failed to create temporary workspace")?; + let root = Utf8PathBuf::from_path_buf(temp_dir.path().to_path_buf()) + .map_err(|path| anyhow!("path `{}` is not UTF-8 encoded", path.display()))?; + + let package_name = format!("{}_example_{}", metadata.name, index); + let manifest_path = root.join("Scarb.toml"); + + let workspace = Self { + _temp_dir: temp_dir, + root, + manifest_path, + package_name, + index, + item_full_path: code_block.id.item_full_path.clone(), + }; + workspace.write_manifest(metadata)?; + workspace.write_src(&code_block.content, &metadata.name)?; + + Ok(workspace) + } + + fn root(&self) -> &Utf8Path { + &self.root + } + + fn manifest_path(&self) -> &Utf8Path { + &self.manifest_path + } + + fn write_manifest(&self, metadata: &PackageMetadata) -> Result<()> { + let package_dir = metadata .manifest_path .parent() .context("package manifest path has no parent directory")?; - let name = self.generated_package_name(index); - let dependency_path = format!("\"{}\"", package_dir); + let dep = &metadata.name; + let dep_path = format!("\"{}\"", package_dir); + let name = &self.package_name; + let edition = edition_variant(Edition::latest()); + let manifest = formatdoc! {r#" [package] name = "{name}" version = "0.1.0" - edition = "2024_07" + edition = "{edition}" [dependencies] - {package_name} = {{ path = {dependency_path} }} + {dep} = {{ path = {dep_path} }} cairo_execute = "{CAIRO_VERSION}" [cairo] enable-gas = false [executable] - "#}; - fs::write(dir.join("Scarb.toml"), manifest) - .context("failed to write manifest for example")?; + "# + }; + fs::write(&self.manifest_path, manifest).context("failed to write manifest for example")?; Ok(()) } - fn write_lib_cairo(&self, dir: &Utf8Path, code_block: &CodeBlock) -> Result<()> { - let package_name = &self.package_metadata.name; - let src_dir = dir.join("src"); + fn write_src(&self, content: &str, package_name: &str) -> Result<()> { + let src_dir = self.root.join("src"); fs::create_dir_all(&src_dir).context("failed to create src directory")?; - let mut body = String::new(); - for line in code_block.content.lines() { - if line.trim().is_empty() { - body.push_str(" \n"); - } else { - body.push_str(" "); - body.push_str(line); - body.push('\n'); - } + let mut body = String::with_capacity(content.len() + content.lines().count() * 5); + for line in content.lines() { + writeln!(body, " {}", line)?; } - let lib_cairo = formatdoc! {r#" use {package_name}::*; @@ -174,76 +312,15 @@ impl<'a> DocTestRunner<'a> { fn main() {{ {body} }} - "# }; + "#}; fs::write(src_dir.join("lib.cairo"), lib_cairo).context("failed to write lib.cairo")?; Ok(()) } - - fn run_execute( - &self, - project_dir: &Utf8Path, - index: usize, - code_block: &CodeBlock, - ) -> Result<(String, String)> { - let target_dir = project_dir.join("target"); - let output_dir = target_dir - .join("execute") - .join(self.generated_package_name(index)); - create_output_dir(output_dir.as_std_path())?; - let (output_dir, execution_id) = incremental_create_execution_output_dir(&output_dir)?; - - self.ui.print(Status::new( - "Executing", - format!("example #{} from `{}`", index, code_block.id.item_full_path).as_str(), - )); - ScarbCommand::new() - .arg("execute") - // .args(["--executable-function", "main"]) - .arg("--save-program-output") - .arg("--save-print-output") - .current_dir(project_dir) - .env("SCARB_EXECUTION_ID", execution_id.to_string()) - .env("SCARB_TARGET_DIR", target_dir.as_str()) - .env("SCARB_UI_VERBOSITY", self.ui.verbosity().to_string()) - .env( - "SCARB_MANIFEST_PATH", - project_dir.join("Scarb.toml").as_str(), - ) - .env("SCARB_ALL_FEATURES", "true") - .run() - .with_context(|| "execution failed")?; - - let print_output_file = output_dir.join(EXECUTE_PRINT_OUTPUT_FILENAME); - let print_output = fs::read_to_string(&print_output_file).with_context(|| { - format!( - "failed to read execution print output from file: {}", - print_output_file - ) - })?; - let program_output_file = output_dir.join(EXECUTE_PROGRAM_OUTPUT_FILENAME); - let program_output = fs::read_to_string(&program_output_file).with_context(|| { - format!( - "failed to read program output from file: {}", - program_output_file - ) - })?; - Ok(( - print_output.trim().to_string(), - program_output.trim().to_string(), - )) - } - - fn generated_package_name(&self, index: usize) -> String { - let package_name = &self.package_metadata.name; - format!("{package_name}_example_{index}") - } } -/// Collects all runnable `DocCodeBlock`s from the crate. -pub fn collect_runnable_code_blocks(crate_: &Crate<'_>) -> Vec { +pub fn collect_code_blocks(crate_: &Crate<'_>) -> Vec { let mut runnable_code_blocks = Vec::new(); collect_from_module(&crate_.root_module, &mut runnable_code_blocks); - // TODO: should these be ignored? for module in &crate_.foreign_crates { collect_from_module(module, &mut runnable_code_blocks); } @@ -263,8 +340,14 @@ fn collect_from_module(module: &Module<'_>, runnable_code_blocks: &mut Vec, runnable_code_blocks: &mut Vec) { for block in &item_data.code_blocks { - if block.run_strategy() == RunStrategy::Run { - runnable_code_blocks.push(block.clone()); - } + runnable_code_blocks.push(block.clone()); } } + +fn edition_variant(edition: Edition) -> String { + let edition = serde_json::to_value(edition).unwrap(); + let serde_json::Value::String(edition) = edition else { + panic!("Edition should always be a string.") + }; + edition +} diff --git a/extensions/scarb-doc/tests/runnable_examples.rs b/extensions/scarb-doc/tests/runnable_examples.rs index a8581a437..c4afc260d 100644 --- a/extensions/scarb-doc/tests/runnable_examples.rs +++ b/extensions/scarb-doc/tests/runnable_examples.rs @@ -25,13 +25,16 @@ fn supports_runnable_examples() { .assert() .success() .stdout_eq(formatdoc! {r#" - [..] Executing example #0 from `hello_world::foo_bar` + [..] Found 3 doc tests; 2 ignored + [..] Running 1 doc tests + [..] Running example #0 from `hello_world::foo_bar` [..] Compiling hello_world_example_0 v0.1.0 ([..]) [..] Finished `dev` profile target(s) in [..] [..] Executing hello_world_example_0 foo bar Saving output to: target/execute/hello_world_example_0/execution1 + [..] Passed example #0 from `hello_world::foo_bar` Saving output to: target/doc/hello_world Saving build output to: target/doc/hello_world/book From 9e31cb3aed35a6c1691e24c800726127ffa2bd91 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Mon, 1 Dec 2025 15:04:20 +0300 Subject: [PATCH 18/32] further refactors --- .../src/docs_generation/markdown/traits.rs | 31 +++++++++++-------- extensions/scarb-doc/src/runner.rs | 5 +-- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/extensions/scarb-doc/src/docs_generation/markdown/traits.rs b/extensions/scarb-doc/src/docs_generation/markdown/traits.rs index 302c05114..441acb220 100644 --- a/extensions/scarb-doc/src/docs_generation/markdown/traits.rs +++ b/extensions/scarb-doc/src/docs_generation/markdown/traits.rs @@ -182,27 +182,32 @@ pub trait MarkdownDocItem: DocItem { } fn get_documentation(&self, context: &MarkdownGenerationContext) -> Option { + let execution_results_map = context + .execution_results() + .map(|results| { + self.code_blocks() + .iter() + .filter_map(|block| { + results + .get(&block.id) + .map(|res| (block.id.close_token_idx, res)) + }) + .collect::>() + }) + .unwrap_or_default(); self.doc().as_ref().map(|doc_tokens| { - // TODO: filter out execution results that do not belong to this item doc_tokens .iter() .enumerate() - .flat_map(|(idx, doc_token)| match doc_token { + .map(|(idx, token)| match token { DocumentationCommentToken::Content(content) => { - // Check if this token is the closing fence of a code block that has execution results - if let Some(cb) = self - .code_blocks() - .iter() - .find(|cb| cb.id.close_token_idx == idx) - && let Some(results) = context.execution_results() - && let Some(res) = results.get(&cb.id) - { - return vec![content.clone(), res.as_markdown()]; + match execution_results_map.get(&idx) { + Some(res) => format!("{}{}", content, res.as_markdown()), + None => content.clone(), } - vec![content.clone()] } DocumentationCommentToken::Link(link) => { - vec![self.format_link_to_path(link, context)] + self.format_link_to_path(link, context) } }) .join("") diff --git a/extensions/scarb-doc/src/runner.rs b/extensions/scarb-doc/src/runner.rs index e2708cc2f..e3efdaac1 100644 --- a/extensions/scarb-doc/src/runner.rs +++ b/extensions/scarb-doc/src/runner.rs @@ -16,8 +16,8 @@ use scarb_metadata::{PackageMetadata, ScarbCommand}; use scarb_ui::Ui; use scarb_ui::components::Status; use std::collections::HashMap; -use std::fs; use std::fmt::Write; +use std::fs; use tempfile::{TempDir, tempdir}; pub type ExecutionResults = HashMap; @@ -126,7 +126,8 @@ impl<'a> TestRunner<'a> { } Err(e) => { summary.failed += 1; - self.ui.error(format!("Error running example #{}: {:#}", index, e)); + self.ui + .error(format!("Error running example #{}: {:#}", index, e)); } } } From 1d6181a509d9019cb7168c240a894ba29723998a Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Mon, 1 Dec 2025 17:15:29 +0300 Subject: [PATCH 19/32] further refactors --- extensions/scarb-doc/src/code_blocks.rs | 32 +++++- extensions/scarb-doc/src/main.rs | 5 +- extensions/scarb-doc/src/runner.rs | 100 ++++++------------ extensions/scarb-doc/src/types/item_data.rs | 8 +- extensions/scarb-doc/src/types/other_types.rs | 99 ++++++++++++++++- 5 files changed, 169 insertions(+), 75 deletions(-) diff --git a/extensions/scarb-doc/src/code_blocks.rs b/extensions/scarb-doc/src/code_blocks.rs index 302133e21..bba8f4b0b 100644 --- a/extensions/scarb-doc/src/code_blocks.rs +++ b/extensions/scarb-doc/src/code_blocks.rs @@ -1,4 +1,7 @@ use crate::runner::{ExecutionOutcome, RunStrategy}; +use crate::types::crate_type::Crate; +use crate::types::module_type::Module; +use crate::types::other_types::ItemData; use cairo_lang_doc::parser::DocumentationCommentToken; use std::str::from_utf8; @@ -114,8 +117,33 @@ impl CodeBlock { } } -/// Collects code blocks from documentation comment tokens. -pub fn collect_code_blocks( +pub fn collect_code_blocks(crate_: &Crate<'_>) -> Vec { + let mut runnable_code_blocks = Vec::new(); + collect_from_module(&crate_.root_module, &mut runnable_code_blocks); + for module in &crate_.foreign_crates { + collect_from_module(module, &mut runnable_code_blocks); + } + // Sort to run deterministically + runnable_code_blocks.sort_by_key(|block| block.id.clone()); + runnable_code_blocks +} + +fn collect_from_module(module: &Module<'_>, runnable_code_blocks: &mut Vec) { + for item_data in module.get_all_item_ids().values() { + collect_from_item_data(item_data, runnable_code_blocks); + } + for item_data in module.pub_uses.get_all_item_ids().values() { + collect_from_item_data(item_data, runnable_code_blocks); + } +} + +fn collect_from_item_data(item_data: &ItemData<'_>, runnable_code_blocks: &mut Vec) { + for block in &item_data.code_blocks { + runnable_code_blocks.push(block.clone()); + } +} + +pub fn collect_code_blocks_from_tokens( doc_tokens: &Option>, full_path: &str, ) -> Vec { diff --git a/extensions/scarb-doc/src/main.rs b/extensions/scarb-doc/src/main.rs index ac8b919b0..8c46e82c1 100644 --- a/extensions/scarb-doc/src/main.rs +++ b/extensions/scarb-doc/src/main.rs @@ -2,12 +2,13 @@ use anyhow::{Result, ensure}; use camino::Utf8PathBuf; use clap::Parser; use mimalloc::MiMalloc; +use scarb_doc::code_blocks::collect_code_blocks; use scarb_doc::diagnostics::print_diagnostics; use scarb_doc::docs_generation::common::OutputFilesExtension; use scarb_doc::docs_generation::markdown::{MarkdownContent, WorkspaceMarkdownBuilder}; use scarb_doc::errors::{MetadataCommandError, PackagesSerializationError}; use scarb_doc::metadata::get_target_dir; -use scarb_doc::runner::{ExecutionResults, TestRunner, collect_code_blocks}; +use scarb_doc::runner::{ExecutionResults, TestRunner}; use scarb_doc::versioned_json_output::VersionedJsonOutput; use scarb_doc::{PackageInformation, generate_package_context, generate_package_information}; use scarb_extensions_cli::doc::{Args, OutputFormat}; @@ -85,7 +86,7 @@ fn run_doc_tests(package: &PackageInformation, ui: &Ui) -> Result TestRunner<'a> { } } - pub fn execute( - &self, - code_blocks: &[CodeBlock], - ) -> Result<(ExecutionSummary, ExecutionResults)> { + pub fn run(&self, code_blocks: &[CodeBlock]) -> Result<(ExecutionSummary, ExecutionResults)> { let mut results = HashMap::new(); let mut summary = ExecutionSummary::default(); let (to_run, ignored): (Vec<_>, Vec<_>) = code_blocks @@ -115,7 +109,7 @@ impl<'a> TestRunner<'a> { )); for (index, code_block) in to_run.iter().enumerate() { let strategy = code_block.run_strategy(); - match self.execute_single(code_block, index, strategy) { + match self.run_single(code_block, index, strategy) { Ok(res) => { if res.status == TestStatus::Passed { summary.passed += 1; @@ -135,18 +129,18 @@ impl<'a> TestRunner<'a> { Ok((summary, results)) } - fn execute_single( + fn run_single( &self, code_block: &CodeBlock, index: usize, strategy: RunStrategy, ) -> Result { - let ws = TestWorkspace::new(self.package_metadata, index, code_block)?; self.ui.print(Status::new( "Running", - format!("example #{} from `{}`", ws.index, ws.item_full_path).as_str(), + &format!("example #{} from `{}`", index, code_block.id.item_full_path), )); - let (actual, print_output, program_output) = self.run_test(&ws, strategy)?; + let ws = TestWorkspace::new(self.package_metadata, index, code_block)?; + let (actual, print_output, program_output) = self.run_single_inner(&ws, strategy)?; let expected = code_block.expected_outcome(); let status = if actual == expected { TestStatus::Passed @@ -156,23 +150,23 @@ impl<'a> TestRunner<'a> { match status { TestStatus::Passed => self.ui.print(Status::new( "Passed", - &format!("example #{} from `{}`", ws.index, ws.item_full_path), + &format!("example #{} from `{}`", index, ws.item_full_path), )), TestStatus::Failed => self.ui.print(Status::new( "Failed", - &format!("example #{} from `{}`", ws.index, ws.item_full_path), + &format!("example #{} from `{}`", index, ws.item_full_path), )), } Ok(ExecutionResult { + outcome: actual, status, print_output, program_output, - outcome: actual, }) } - fn run_test( + fn run_single_inner( &self, ws: &TestWorkspace, strategy: RunStrategy, @@ -189,12 +183,13 @@ impl<'a> TestRunner<'a> { .env("SCARB_MANIFEST_PATH", ws.manifest_path().as_str()) .env("SCARB_ALL_FEATURES", "true") .run(); + if build_result.is_err() { return Ok((ExecutionOutcome::CompileError, String::new(), String::new())); + } else if strategy == RunStrategy::Build { + return Ok((ExecutionOutcome::BuildSuccess, String::new(), String::new())); } - if strategy == RunStrategy::Build { - return Ok((ExecutionOutcome::RunSuccess, String::new(), String::new())); - } + let output_dir = target_dir.join("execute").join(&ws.package_name); create_output_dir(output_dir.as_std_path())?; let (output_dir, execution_id) = incremental_create_execution_output_dir(&output_dir)?; @@ -213,44 +208,45 @@ impl<'a> TestRunner<'a> { .run(); if run_result.is_err() { - return Ok((ExecutionOutcome::RuntimeError, String::new(), String::new())); + Ok((ExecutionOutcome::RuntimeError, String::new(), String::new())) + } else { + let print_output = fs::read_to_string(output_dir.join(EXECUTE_PRINT_OUTPUT_FILENAME)) + .unwrap_or_default() + .trim() + .to_string(); + let program_output = + fs::read_to_string(output_dir.join(EXECUTE_PROGRAM_OUTPUT_FILENAME)) + .unwrap_or_default() + .trim() + .to_string(); + Ok((ExecutionOutcome::RunSuccess, print_output, program_output)) } - let print_output = fs::read_to_string(output_dir.join(EXECUTE_PRINT_OUTPUT_FILENAME)) - .unwrap_or_default() - .trim() - .to_string(); - let program_output = fs::read_to_string(output_dir.join(EXECUTE_PROGRAM_OUTPUT_FILENAME)) - .unwrap_or_default() - .trim() - .to_string(); - Ok((ExecutionOutcome::RunSuccess, print_output, program_output)) } } struct TestWorkspace { _temp_dir: TempDir, root: Utf8PathBuf, - manifest_path: Utf8PathBuf, package_name: String, - index: usize, item_full_path: String, } impl TestWorkspace { - fn new(metadata: &PackageMetadata, index: usize, code_block: &CodeBlock) -> Result { + pub(crate) fn new( + metadata: &PackageMetadata, + index: usize, + code_block: &CodeBlock, + ) -> Result { let temp_dir = tempdir().context("failed to create temporary workspace")?; let root = Utf8PathBuf::from_path_buf(temp_dir.path().to_path_buf()) .map_err(|path| anyhow!("path `{}` is not UTF-8 encoded", path.display()))?; let package_name = format!("{}_example_{}", metadata.name, index); - let manifest_path = root.join("Scarb.toml"); let workspace = Self { _temp_dir: temp_dir, root, - manifest_path, package_name, - index, item_full_path: code_block.id.item_full_path.clone(), }; workspace.write_manifest(metadata)?; @@ -263,8 +259,8 @@ impl TestWorkspace { &self.root } - fn manifest_path(&self) -> &Utf8Path { - &self.manifest_path + fn manifest_path(&self) -> Utf8PathBuf { + self.root.join("Scarb.toml") } fn write_manifest(&self, metadata: &PackageMetadata) -> Result<()> { @@ -294,12 +290,12 @@ impl TestWorkspace { [executable] "# }; - fs::write(&self.manifest_path, manifest).context("failed to write manifest for example")?; + fs::write(&self.manifest_path(), manifest).context("failed to write manifest")?; Ok(()) } fn write_src(&self, content: &str, package_name: &str) -> Result<()> { - let src_dir = self.root.join("src"); + let src_dir = self.root().join("src"); fs::create_dir_all(&src_dir).context("failed to create src directory")?; let mut body = String::with_capacity(content.len() + content.lines().count() * 5); @@ -319,32 +315,6 @@ impl TestWorkspace { } } -pub fn collect_code_blocks(crate_: &Crate<'_>) -> Vec { - let mut runnable_code_blocks = Vec::new(); - collect_from_module(&crate_.root_module, &mut runnable_code_blocks); - for module in &crate_.foreign_crates { - collect_from_module(module, &mut runnable_code_blocks); - } - // Sort to run deterministically - runnable_code_blocks.sort_by_key(|block| block.id.clone()); - runnable_code_blocks -} - -fn collect_from_module(module: &Module<'_>, runnable_code_blocks: &mut Vec) { - for item_data in module.get_all_item_ids().values() { - collect_from_item_data(item_data, runnable_code_blocks); - } - for item_data in module.pub_uses.get_all_item_ids().values() { - collect_from_item_data(item_data, runnable_code_blocks); - } -} - -fn collect_from_item_data(item_data: &ItemData<'_>, runnable_code_blocks: &mut Vec) { - for block in &item_data.code_blocks { - runnable_code_blocks.push(block.clone()); - } -} - fn edition_variant(edition: Edition) -> String { let edition = serde_json::to_value(edition).unwrap(); let serde_json::Value::String(edition) = edition else { diff --git a/extensions/scarb-doc/src/types/item_data.rs b/extensions/scarb-doc/src/types/item_data.rs index eecaa95b9..59d959731 100644 --- a/extensions/scarb-doc/src/types/item_data.rs +++ b/extensions/scarb-doc/src/types/item_data.rs @@ -10,7 +10,7 @@ use cairo_lang_filesystem::ids::CrateId; use serde::Serialize; use serde::Serializer; use std::fmt::Debug; -use crate::code_blocks::{collect_code_blocks, CodeBlock}; +use crate::code_blocks::{collect_code_blocks, collect_code_blocks_from_tokens, CodeBlock}; #[derive(Debug, Serialize, Clone)] pub struct ItemData<'db> { @@ -46,7 +46,7 @@ impl<'db> ItemData<'db> { let group = find_groups_from_attributes(db, &id); let full_path = id.full_path(db); let doc = db.get_item_documentation_as_tokens(documentable_item_id); - let code_blocks = collect_code_blocks(&doc, &full_path); + let code_blocks = collect_code_blocks_from_tokens(&doc, &full_path); Self { id: documentable_item_id, @@ -72,7 +72,7 @@ impl<'db> ItemData<'db> { id.name(db).long(db) ); let doc = db.get_item_documentation_as_tokens(documentable_item_id); - let code_blocks = collect_code_blocks(&doc, &full_path); + let code_blocks = collect_code_blocks_from_tokens(&doc, &full_path); Self { id: documentable_item_id, @@ -91,7 +91,7 @@ impl<'db> ItemData<'db> { let documentable_id = DocumentableItemId::Crate(id); let full_path = ModuleId::CrateRoot(id).full_path(db); let doc = db.get_item_documentation_as_tokens(documentable_id); - let code_blocks = collect_code_blocks(&doc, &full_path); + let code_blocks = collect_code_blocks_from_tokens(&doc, &full_path); Self { id: documentable_id, diff --git a/extensions/scarb-doc/src/types/other_types.rs b/extensions/scarb-doc/src/types/other_types.rs index 08bdd4fe9..df68c33ce 100644 --- a/extensions/scarb-doc/src/types/other_types.rs +++ b/extensions/scarb-doc/src/types/other_types.rs @@ -1,7 +1,7 @@ use anyhow::Result; use crate::attributes::find_groups_from_attributes; -use crate::code_blocks::{CodeBlock, collect_code_blocks}; +use crate::code_blocks::{CodeBlock, collect_code_blocks_from_tokens}; use crate::db::ScarbDocDatabase; use crate::docs_generation::markdown::context::IncludedItems; use crate::docs_generation::markdown::traits::WithPath; @@ -41,7 +41,6 @@ pub struct ItemData<'db> { } /// Mimics the [`TopLevelLanguageElementId::full_path`] but skips the macro modules. -/// Mimics the [`cairo_lang_defs::ids::TopLevelLanguageElementId::full_path`] but skips the macro modules. /// If not omitted, the path would look like, for example, /// `hello::define_fn_outter!(func_macro_fn_outter);::expose! {\n\t\t\tpub fn func_macro_fn_outter() -> felt252 { \n\t\t\t\tprintln!(\"hello world\");\n\t\t\t\t10 }\n\t\t}::func_macro_fn_outter` pub fn doc_full_path(module_id: &ModuleId, db: &ScarbDocDatabase) -> String { @@ -58,6 +57,102 @@ pub fn doc_full_path(module_id: &ModuleId, db: &ScarbDocDatabase) -> String { } } +impl<'db> ItemData<'db> { + pub fn new( + db: &'db ScarbDocDatabase, + id: impl TopLevelLanguageElementId<'db>, + documentable_item_id: DocumentableItemId<'db>, + parent_full_path: String, + ) -> Self { + let (signature, doc_location_links) = + db.get_item_signature_with_links(documentable_item_id); + let doc_location_links = doc_location_links + .iter() + .map(|link| DocLocationLink::new(link.start, link.end, link.item_id, db)) + .collect::>(); + let group = find_groups_from_attributes(db, &id); + let full_path = id.full_path(db); + let doc = db.get_item_documentation_as_tokens(documentable_item_id); + let code_blocks = collect_code_blocks_from_tokens(&doc, &full_path); + + Self { + id: documentable_item_id, + name: id.name(db).to_string(db), + doc, + signature, + full_path: format!("{}::{}", parent_full_path, id.name(db).long(db)), + parent_full_path: Some(parent_full_path), + code_blocks, + doc_location_links, + group, + } + } + + pub fn new_without_signature( + db: &'db ScarbDocDatabase, + id: impl TopLevelLanguageElementId<'db>, + documentable_item_id: DocumentableItemId<'db>, + ) -> Self { + let full_path = format!( + "{}::{}", + doc_full_path(&id.parent_module(db), db), + id.name(db).long(db) + ); + let doc = db.get_item_documentation_as_tokens(documentable_item_id); + let code_blocks = collect_code_blocks_from_tokens(&doc, &full_path); + + Self { + id: documentable_item_id, + name: id.name(db).to_string(db), + doc, + signature: None, + full_path, + parent_full_path: Some(id.parent_module(db).full_path(db)), + code_blocks, + doc_location_links: vec![], + group: find_groups_from_attributes(db, &id), + } + } + + pub fn new_crate(db: &'db ScarbDocDatabase, id: CrateId<'db>) -> Self { + let documentable_id = DocumentableItemId::Crate(id); + let full_path = ModuleId::CrateRoot(id).full_path(db); + let doc = db.get_item_documentation_as_tokens(documentable_id); + let code_blocks = collect_code_blocks_from_tokens(&doc, &full_path); + + Self { + id: documentable_id, + name: id.long(db).name().to_string(db), + doc, + signature: None, + full_path, + parent_full_path: None, + code_blocks, + doc_location_links: vec![], + group: None, + } + } +} + +fn documentation_serializer( + docs: &Option>, + serializer: S, +) -> Result +where + S: Serializer, +{ + match docs { + Some(doc_vec) => { + let combined = doc_vec + .iter() + .map(|dct| dct.to_string()) + .collect::>(); + serializer.serialize_str(&combined.join("")) + } + None => serializer.serialize_none(), + } +} + #[derive(Serialize, Clone)] pub struct Constant<'db> { #[serde(skip)] From 9d32adf840104e35996dffce8e6c49799f41b136 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Mon, 1 Dec 2025 17:32:09 +0300 Subject: [PATCH 20/32] further refactors --- extensions/scarb-doc/src/doc_test.rs | 3 + .../src/{ => doc_test}/code_blocks.rs | 2 +- .../scarb-doc/src/{ => doc_test}/runner.rs | 116 +----------------- .../scarb-doc/src/doc_test/workspace.rs | 113 +++++++++++++++++ extensions/scarb-doc/src/docs_generation.rs | 2 +- .../scarb-doc/src/docs_generation/markdown.rs | 2 +- .../src/docs_generation/markdown/context.rs | 2 +- .../src/docs_generation/markdown/summary.rs | 2 +- extensions/scarb-doc/src/lib.rs | 3 +- extensions/scarb-doc/src/main.rs | 4 +- extensions/scarb-doc/src/types/other_types.rs | 2 +- 11 files changed, 131 insertions(+), 120 deletions(-) create mode 100644 extensions/scarb-doc/src/doc_test.rs rename extensions/scarb-doc/src/{ => doc_test}/code_blocks.rs (99%) rename extensions/scarb-doc/src/{ => doc_test}/runner.rs (68%) create mode 100644 extensions/scarb-doc/src/doc_test/workspace.rs diff --git a/extensions/scarb-doc/src/doc_test.rs b/extensions/scarb-doc/src/doc_test.rs new file mode 100644 index 000000000..c56782f92 --- /dev/null +++ b/extensions/scarb-doc/src/doc_test.rs @@ -0,0 +1,3 @@ +pub mod code_blocks; +pub mod runner; +pub mod workspace; diff --git a/extensions/scarb-doc/src/code_blocks.rs b/extensions/scarb-doc/src/doc_test/code_blocks.rs similarity index 99% rename from extensions/scarb-doc/src/code_blocks.rs rename to extensions/scarb-doc/src/doc_test/code_blocks.rs index bba8f4b0b..2e3f27427 100644 --- a/extensions/scarb-doc/src/code_blocks.rs +++ b/extensions/scarb-doc/src/doc_test/code_blocks.rs @@ -1,4 +1,4 @@ -use crate::runner::{ExecutionOutcome, RunStrategy}; +use crate::doc_test::runner::{ExecutionOutcome, RunStrategy}; use crate::types::crate_type::Crate; use crate::types::module_type::Module; use crate::types::other_types::ItemData; diff --git a/extensions/scarb-doc/src/runner.rs b/extensions/scarb-doc/src/doc_test/runner.rs similarity index 68% rename from extensions/scarb-doc/src/runner.rs rename to extensions/scarb-doc/src/doc_test/runner.rs index 7049563d5..37af2b687 100644 --- a/extensions/scarb-doc/src/runner.rs +++ b/extensions/scarb-doc/src/doc_test/runner.rs @@ -1,10 +1,7 @@ -use crate::code_blocks::{CodeBlock, CodeBlockId}; -use anyhow::{Context, Result, anyhow}; -use cairo_lang_filesystem::db::Edition; -use camino::{Utf8Path, Utf8PathBuf}; +use crate::doc_test::code_blocks::{CodeBlock, CodeBlockId}; +use crate::doc_test::workspace::TestWorkspace; +use anyhow::Result; use create_output_dir::create_output_dir; -use indoc::formatdoc; -use scarb_build_metadata::CAIRO_VERSION; use scarb_execute_utils::{ EXECUTE_PRINT_OUTPUT_FILENAME, EXECUTE_PROGRAM_OUTPUT_FILENAME, incremental_create_execution_output_dir, @@ -13,9 +10,7 @@ use scarb_metadata::{PackageMetadata, ScarbCommand}; use scarb_ui::Ui; use scarb_ui::components::Status; use std::collections::HashMap; -use std::fmt::Write; use std::fs; -use tempfile::{TempDir, tempdir}; pub type ExecutionResults = HashMap; @@ -150,11 +145,11 @@ impl<'a> TestRunner<'a> { match status { TestStatus::Passed => self.ui.print(Status::new( "Passed", - &format!("example #{} from `{}`", index, ws.item_full_path), + &format!("example #{} from `{}`", index, ws.item_full_path()), )), TestStatus::Failed => self.ui.print(Status::new( "Failed", - &format!("example #{} from `{}`", index, ws.item_full_path), + &format!("example #{} from `{}`", index, ws.item_full_path()), )), } @@ -190,7 +185,7 @@ impl<'a> TestRunner<'a> { return Ok((ExecutionOutcome::BuildSuccess, String::new(), String::new())); } - let output_dir = target_dir.join("execute").join(&ws.package_name); + let output_dir = target_dir.join("execute").join(&ws.package_name()); create_output_dir(output_dir.as_std_path())?; let (output_dir, execution_id) = incremental_create_execution_output_dir(&output_dir)?; @@ -223,102 +218,3 @@ impl<'a> TestRunner<'a> { } } } - -struct TestWorkspace { - _temp_dir: TempDir, - root: Utf8PathBuf, - package_name: String, - item_full_path: String, -} - -impl TestWorkspace { - pub(crate) fn new( - metadata: &PackageMetadata, - index: usize, - code_block: &CodeBlock, - ) -> Result { - let temp_dir = tempdir().context("failed to create temporary workspace")?; - let root = Utf8PathBuf::from_path_buf(temp_dir.path().to_path_buf()) - .map_err(|path| anyhow!("path `{}` is not UTF-8 encoded", path.display()))?; - - let package_name = format!("{}_example_{}", metadata.name, index); - - let workspace = Self { - _temp_dir: temp_dir, - root, - package_name, - item_full_path: code_block.id.item_full_path.clone(), - }; - workspace.write_manifest(metadata)?; - workspace.write_src(&code_block.content, &metadata.name)?; - - Ok(workspace) - } - - fn root(&self) -> &Utf8Path { - &self.root - } - - fn manifest_path(&self) -> Utf8PathBuf { - self.root.join("Scarb.toml") - } - - fn write_manifest(&self, metadata: &PackageMetadata) -> Result<()> { - let package_dir = metadata - .manifest_path - .parent() - .context("package manifest path has no parent directory")?; - - let dep = &metadata.name; - let dep_path = format!("\"{}\"", package_dir); - let name = &self.package_name; - let edition = edition_variant(Edition::latest()); - - let manifest = formatdoc! {r#" - [package] - name = "{name}" - version = "0.1.0" - edition = "{edition}" - - [dependencies] - {dep} = {{ path = {dep_path} }} - cairo_execute = "{CAIRO_VERSION}" - - [cairo] - enable-gas = false - - [executable] - "# - }; - fs::write(&self.manifest_path(), manifest).context("failed to write manifest")?; - Ok(()) - } - - fn write_src(&self, content: &str, package_name: &str) -> Result<()> { - let src_dir = self.root().join("src"); - fs::create_dir_all(&src_dir).context("failed to create src directory")?; - - let mut body = String::with_capacity(content.len() + content.lines().count() * 5); - for line in content.lines() { - writeln!(body, " {}", line)?; - } - let lib_cairo = formatdoc! {r#" - use {package_name}::*; - - #[executable] - fn main() {{ - {body} - }} - "#}; - fs::write(src_dir.join("lib.cairo"), lib_cairo).context("failed to write lib.cairo")?; - Ok(()) - } -} - -fn edition_variant(edition: Edition) -> String { - let edition = serde_json::to_value(edition).unwrap(); - let serde_json::Value::String(edition) = edition else { - panic!("Edition should always be a string.") - }; - edition -} diff --git a/extensions/scarb-doc/src/doc_test/workspace.rs b/extensions/scarb-doc/src/doc_test/workspace.rs new file mode 100644 index 000000000..4ad75d01a --- /dev/null +++ b/extensions/scarb-doc/src/doc_test/workspace.rs @@ -0,0 +1,113 @@ +use crate::doc_test::code_blocks::CodeBlock; +use anyhow::{Context, Result, anyhow}; +use cairo_lang_filesystem::db::Edition; +use camino::{Utf8Path, Utf8PathBuf}; +use indoc::formatdoc; +use scarb_build_metadata::CAIRO_VERSION; +use scarb_metadata::PackageMetadata; +use std::fmt::Write; +use std::fs; +use tempfile::{TempDir, tempdir}; + +pub struct TestWorkspace { + _temp_dir: TempDir, + root: Utf8PathBuf, + package_name: String, + item_full_path: String, +} + +impl TestWorkspace { + pub fn new(metadata: &PackageMetadata, index: usize, code_block: &CodeBlock) -> Result { + let temp_dir = tempdir().context("failed to create temporary workspace")?; + let root = Utf8PathBuf::from_path_buf(temp_dir.path().to_path_buf()) + .map_err(|path| anyhow!("path `{}` is not UTF-8 encoded", path.display()))?; + + let package_name = format!("{}_example_{}", metadata.name, index); + + let workspace = Self { + _temp_dir: temp_dir, + root, + package_name, + item_full_path: code_block.id.item_full_path.clone(), + }; + workspace.write_manifest(metadata)?; + workspace.write_src(&code_block.content, &metadata.name)?; + + Ok(workspace) + } + + pub fn root(&self) -> &Utf8Path { + &self.root + } + + pub fn manifest_path(&self) -> Utf8PathBuf { + self.root.join("Scarb.toml") + } + + pub fn package_name(&self) -> &str { + &self.package_name + } + + pub fn item_full_path(&self) -> &str { + &self.item_full_path + } + + fn write_manifest(&self, metadata: &PackageMetadata) -> Result<()> { + let package_dir = metadata + .manifest_path + .parent() + .context("package manifest path has no parent directory")?; + + let dep = &metadata.name; + let dep_path = format!("\"{}\"", package_dir); + let name = &self.package_name; + let edition = edition_variant(Edition::latest()); + + let manifest = formatdoc! {r#" + [package] + name = "{name}" + version = "0.1.0" + edition = "{edition}" + + [dependencies] + {dep} = {{ path = {dep_path} }} + cairo_execute = "{CAIRO_VERSION}" + + [cairo] + enable-gas = false + + [executable] + "# + }; + fs::write(&self.manifest_path(), manifest).context("failed to write manifest")?; + Ok(()) + } + + fn write_src(&self, content: &str, package_name: &str) -> Result<()> { + let src_dir = self.root().join("src"); + fs::create_dir_all(&src_dir).context("failed to create src directory")?; + + let mut body = String::with_capacity(content.len() + content.lines().count() * 5); + for line in content.lines() { + writeln!(body, " {}", line)?; + } + let lib_cairo = formatdoc! {r#" + use {package_name}::*; + + #[executable] + fn main() {{ + {body} + }} + "#}; + fs::write(src_dir.join("lib.cairo"), lib_cairo).context("failed to write lib.cairo")?; + Ok(()) + } +} + +fn edition_variant(edition: Edition) -> String { + let edition = serde_json::to_value(edition).unwrap(); + let serde_json::Value::String(edition) = edition else { + panic!("Edition should always be a string.") + }; + edition +} diff --git a/extensions/scarb-doc/src/docs_generation.rs b/extensions/scarb-doc/src/docs_generation.rs index 17245b3c9..c1e248b36 100644 --- a/extensions/scarb-doc/src/docs_generation.rs +++ b/extensions/scarb-doc/src/docs_generation.rs @@ -1,4 +1,4 @@ -use crate::code_blocks::CodeBlock; +use crate::doc_test::code_blocks::CodeBlock; use crate::location_links::DocLocationLink; use crate::types::module_type::Module; use crate::types::other_types::{ diff --git a/extensions/scarb-doc/src/docs_generation/markdown.rs b/extensions/scarb-doc/src/docs_generation/markdown.rs index 13fde0400..5d0211912 100644 --- a/extensions/scarb-doc/src/docs_generation/markdown.rs +++ b/extensions/scarb-doc/src/docs_generation/markdown.rs @@ -11,10 +11,10 @@ mod book_toml; pub mod context; mod summary; pub mod traits; +use crate::doc_test::runner::ExecutionResults; use crate::docs_generation::common::{ GeneratedFile, OutputFilesExtension, SummaryIndexMap, SummaryListItem, }; -use crate::runner::ExecutionResults; use std::ops::Add; const BASE_HEADER_LEVEL: usize = 1; diff --git a/extensions/scarb-doc/src/docs_generation/markdown/context.rs b/extensions/scarb-doc/src/docs_generation/markdown/context.rs index 009d52ed3..53a353599 100644 --- a/extensions/scarb-doc/src/docs_generation/markdown/context.rs +++ b/extensions/scarb-doc/src/docs_generation/markdown/context.rs @@ -1,8 +1,8 @@ +use crate::doc_test::runner::ExecutionResults; use crate::docs_generation::common::{OutputFilesExtension, SummaryIndexMap}; use crate::docs_generation::markdown::SUMMARY_FILENAME; use crate::docs_generation::markdown::traits::WithPath; use crate::location_links::DocLocationLink; -use crate::runner::ExecutionResults; use crate::types::crate_type::Crate; use cairo_lang_defs::ids::{ImplItemId, LookupItemId, TraitItemId}; use cairo_lang_doc::documentable_item::DocumentableItemId; diff --git a/extensions/scarb-doc/src/docs_generation/markdown/summary.rs b/extensions/scarb-doc/src/docs_generation/markdown/summary.rs index 80440479c..7de5ba144 100644 --- a/extensions/scarb-doc/src/docs_generation/markdown/summary.rs +++ b/extensions/scarb-doc/src/docs_generation/markdown/summary.rs @@ -2,6 +2,7 @@ pub mod content; pub mod files; pub mod group_files; +use crate::doc_test::runner::ExecutionResults; use crate::docs_generation::common::OutputFilesExtension; use crate::docs_generation::markdown::context::MarkdownGenerationContext; use crate::docs_generation::markdown::summary::content::{ @@ -13,7 +14,6 @@ use crate::docs_generation::markdown::summary::files::{ }; use crate::docs_generation::markdown::traits::{MarkdownDocItem, TopLevelMarkdownDocItem}; use crate::docs_generation::markdown::{BASE_HEADER_LEVEL, SummaryIndexMap}; -use crate::runner::ExecutionResults; use crate::types::crate_type::Crate; use anyhow::Result; use group_files::generate_global_groups_summary_files; diff --git a/extensions/scarb-doc/src/lib.rs b/extensions/scarb-doc/src/lib.rs index ea803ac5c..a37b70671 100644 --- a/extensions/scarb-doc/src/lib.rs +++ b/extensions/scarb-doc/src/lib.rs @@ -26,14 +26,13 @@ use scarb_ui::Ui; use serde::Serialize; pub mod attributes; -pub mod code_blocks; pub mod db; pub mod diagnostics; +pub mod doc_test; pub mod docs_generation; pub mod errors; pub mod location_links; pub mod metadata; -pub mod runner; pub mod types; pub mod versioned_json_output; diff --git a/extensions/scarb-doc/src/main.rs b/extensions/scarb-doc/src/main.rs index 8c46e82c1..762f85525 100644 --- a/extensions/scarb-doc/src/main.rs +++ b/extensions/scarb-doc/src/main.rs @@ -2,13 +2,13 @@ use anyhow::{Result, ensure}; use camino::Utf8PathBuf; use clap::Parser; use mimalloc::MiMalloc; -use scarb_doc::code_blocks::collect_code_blocks; use scarb_doc::diagnostics::print_diagnostics; +use scarb_doc::doc_test::code_blocks::collect_code_blocks; +use scarb_doc::doc_test::runner::{ExecutionResults, TestRunner}; use scarb_doc::docs_generation::common::OutputFilesExtension; use scarb_doc::docs_generation::markdown::{MarkdownContent, WorkspaceMarkdownBuilder}; use scarb_doc::errors::{MetadataCommandError, PackagesSerializationError}; use scarb_doc::metadata::get_target_dir; -use scarb_doc::runner::{ExecutionResults, TestRunner}; use scarb_doc::versioned_json_output::VersionedJsonOutput; use scarb_doc::{PackageInformation, generate_package_context, generate_package_information}; use scarb_extensions_cli::doc::{Args, OutputFormat}; diff --git a/extensions/scarb-doc/src/types/other_types.rs b/extensions/scarb-doc/src/types/other_types.rs index df68c33ce..5abfa6b96 100644 --- a/extensions/scarb-doc/src/types/other_types.rs +++ b/extensions/scarb-doc/src/types/other_types.rs @@ -1,8 +1,8 @@ use anyhow::Result; use crate::attributes::find_groups_from_attributes; -use crate::code_blocks::{CodeBlock, collect_code_blocks_from_tokens}; use crate::db::ScarbDocDatabase; +use crate::doc_test::code_blocks::{CodeBlock, collect_code_blocks_from_tokens}; use crate::docs_generation::markdown::context::IncludedItems; use crate::docs_generation::markdown::traits::WithPath; use crate::types::item_data::{ItemData, SubItemData}; From b6107b6b5b58c633a5829642de1a21654f30a533 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Tue, 2 Dec 2025 04:52:17 +0300 Subject: [PATCH 21/32] further refactors --- Cargo.lock | 1 + extensions/scarb-doc/Cargo.toml | 1 + .../scarb-doc/src/doc_test/code_blocks.rs | 35 ++++- extensions/scarb-doc/src/doc_test/runner.rs | 137 +++++++++++------- .../scarb-doc/src/doc_test/workspace.rs | 37 +++-- extensions/scarb-doc/src/main.rs | 5 +- extensions/scarb-doc/tests/code/code_13.cairo | 13 ++ extensions/scarb-doc/tests/code/code_14.cairo | 13 ++ extensions/scarb-doc/tests/code/code_15.cairo | 20 +++ .../book.toml | 19 +++ .../src/SUMMARY.md | 3 + .../src/hello_world-add.md | 30 ++++ .../src/hello_world-free_functions.md | 6 + .../src/hello_world.md | 10 ++ .../scarb-doc/tests/runnable_examples.rs | 127 +++++++++++++++- utils/scarb-ui/src/components/mod.rs | 2 + utils/scarb-ui/src/components/test_result.rs | 101 +++++++++++++ 17 files changed, 483 insertions(+), 77 deletions(-) create mode 100644 extensions/scarb-doc/tests/code/code_13.cairo create mode 100644 extensions/scarb-doc/tests/code/code_14.cairo create mode 100644 extensions/scarb-doc/tests/code/code_15.cairo create mode 100644 extensions/scarb-doc/tests/data/runnable_examples_multiple_per_item/book.toml create mode 100644 extensions/scarb-doc/tests/data/runnable_examples_multiple_per_item/src/SUMMARY.md create mode 100644 extensions/scarb-doc/tests/data/runnable_examples_multiple_per_item/src/hello_world-add.md create mode 100644 extensions/scarb-doc/tests/data/runnable_examples_multiple_per_item/src/hello_world-free_functions.md create mode 100644 extensions/scarb-doc/tests/data/runnable_examples_multiple_per_item/src/hello_world.md create mode 100644 utils/scarb-ui/src/components/test_result.rs diff --git a/Cargo.lock b/Cargo.lock index 3c364fdc3..602fde9bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6926,6 +6926,7 @@ dependencies = [ "cairo-lang-utils", "camino", "clap", + "console 0.16.1", "create-output-dir", "expect-test", "indoc", diff --git a/extensions/scarb-doc/Cargo.toml b/extensions/scarb-doc/Cargo.toml index fc1db3465..f1e91445f 100644 --- a/extensions/scarb-doc/Cargo.toml +++ b/extensions/scarb-doc/Cargo.toml @@ -22,6 +22,7 @@ cairo-lang-starknet.workspace = true cairo-lang-syntax.workspace = true cairo-lang-utils.workspace = true create-output-dir = { path = "../../utils/create-output-dir" } +console.workspace = true expect-test.workspace = true indoc.workspace = true itertools.workspace = true diff --git a/extensions/scarb-doc/src/doc_test/code_blocks.rs b/extensions/scarb-doc/src/doc_test/code_blocks.rs index 2e3f27427..79c7b057c 100644 --- a/extensions/scarb-doc/src/doc_test/code_blocks.rs +++ b/extensions/scarb-doc/src/doc_test/code_blocks.rs @@ -3,21 +3,35 @@ use crate::types::crate_type::Crate; use crate::types::module_type::Module; use crate::types::other_types::ItemData; use cairo_lang_doc::parser::DocumentationCommentToken; +use std::collections::HashMap; use std::str::from_utf8; #[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] pub struct CodeBlockId { pub item_full_path: String, pub close_token_idx: usize, + /// Index of this block in the item's documentation. + pub block_index: usize, } impl CodeBlockId { - pub fn new(item_full_path: String, close_token_idx: usize) -> Self { + pub fn new(item_full_path: String, block_index: usize, close_token_idx: usize) -> Self { Self { item_full_path, + block_index, close_token_idx, } } + + // TODO: ideally, this should be replaced with logic that + // tracks the exact line of the code block in the source file. + pub fn display_name(&self, total_blocks_in_item: usize) -> String { + if total_blocks_in_item <= 1 { + self.item_full_path.clone() + } else { + format!("{} (example {})", self.item_full_path, self.block_index) + } + } } #[derive(Clone, Debug, PartialEq, Eq)] @@ -54,7 +68,7 @@ pub struct CodeBlock { } impl CodeBlock { - pub fn new(id: CodeBlockId, content: String, info_string: &String) -> Self { + pub fn new(id: CodeBlockId, content: String, info_string: &str) -> Self { let attributes = Self::parse_attributes(info_string); Self { id, @@ -128,6 +142,19 @@ pub fn collect_code_blocks(crate_: &Crate<'_>) -> Vec { runnable_code_blocks } +/// Counts the number of code blocks per documented item. +/// This is used to generate display names for code blocks, +/// allowing to distinguish between multiple code blocks in the same item. +/// +/// Returns the mapping from `item_full_path` to the number of code blocks in that item. +pub fn count_blocks_per_item(code_blocks: &[CodeBlock]) -> HashMap { + let mut counts = HashMap::new(); + for block in code_blocks { + *counts.entry(block.id.item_full_path.clone()).or_insert(0) += 1; + } + counts +} + fn collect_from_module(module: &Module<'_>, runnable_code_blocks: &mut Vec) { for item_data in module.get_all_item_ids().values() { collect_from_item_data(item_data, runnable_code_blocks); @@ -161,6 +188,7 @@ pub fn collect_code_blocks_from_tokens( let mut code_blocks = Vec::new(); let mut current_fence: Option = None; + let mut block_index: usize = 0; for (idx, token) in tokens.iter().enumerate() { let content = match token { @@ -178,13 +206,14 @@ pub fn collect_code_blocks_from_tokens( let body = get_block_body(tokens, opening.token_idx + 1, end_idx); // Skip empty code blocks. - let id = CodeBlockId::new(full_path.to_string(), end_idx); if !body.is_empty() { + let id = CodeBlockId::new(full_path.to_string(), block_index, end_idx); code_blocks.push(CodeBlock::new( id, body.to_string(), &opening.info_string, )); + block_index += 1; } current_fence = None; } diff --git a/extensions/scarb-doc/src/doc_test/runner.rs b/extensions/scarb-doc/src/doc_test/runner.rs index 37af2b687..93987967a 100644 --- a/extensions/scarb-doc/src/doc_test/runner.rs +++ b/extensions/scarb-doc/src/doc_test/runner.rs @@ -1,14 +1,16 @@ -use crate::doc_test::code_blocks::{CodeBlock, CodeBlockId}; +use crate::doc_test::code_blocks::{CodeBlock, CodeBlockId, count_blocks_per_item}; use crate::doc_test::workspace::TestWorkspace; use anyhow::Result; +use console::Style; use create_output_dir::create_output_dir; use scarb_execute_utils::{ EXECUTE_PRINT_OUTPUT_FILENAME, EXECUTE_PROGRAM_OUTPUT_FILENAME, incremental_create_execution_output_dir, }; use scarb_metadata::{PackageMetadata, ScarbCommand}; -use scarb_ui::Ui; -use scarb_ui::components::Status; +use scarb_ui::components::{NewLine, Status, TestResult}; +use scarb_ui::{Message, Ui}; +use serde::{Serialize, Serializer}; use std::collections::HashMap; use std::fs; @@ -43,13 +45,44 @@ impl ExecutionResult { } } -#[derive(Debug, Clone, Default)] -pub struct ExecutionSummary { +#[derive(Debug, Clone, Default, Serialize)] +pub struct TestSummary { pub passed: usize, pub failed: usize, pub ignored: usize, } +impl TestSummary { + pub fn is_ok(&self) -> bool { + self.failed == 0 + } + + pub fn is_fail(&self) -> bool { + self.failed > 0 + } +} + +impl Message for TestSummary { + fn text(self) -> String { + let (status_word, style) = if self.failed == 0 { + ("ok", Style::new().green()) + } else { + ("FAILED", Style::new().red()) + }; + format!( + "test result: {}. {} passed; {} failed; {} ignored", + style.apply_to(status_word), + self.passed, + self.failed, + self.ignored + ) + } + + fn structured(self, ser: S) -> Result { + self.serialize(ser) + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum TestStatus { Passed, @@ -87,54 +120,66 @@ impl<'a> TestRunner<'a> { } } - pub fn run(&self, code_blocks: &[CodeBlock]) -> Result<(ExecutionSummary, ExecutionResults)> { + /// Run all the code code blocks for a given package. + pub fn run_all(&self, code_blocks: &[CodeBlock]) -> Result<(TestSummary, ExecutionResults)> { + let pkg_name = &self.package_metadata.name; + let mut results = HashMap::new(); - let mut summary = ExecutionSummary::default(); - let (to_run, ignored): (Vec<_>, Vec<_>) = code_blocks - .iter() - .partition(|block| block.run_strategy() != RunStrategy::Ignore); + let mut summary = TestSummary::default(); + let mut failed_names = Vec::new(); + let blocks_per_item = count_blocks_per_item(code_blocks); - self.ui.print(Status::new( - "Found", - &format!("{} doc tests; {} ignored", code_blocks.len(), ignored.len(),), - )); self.ui.print(Status::new( "Running", - &format!("{} doc tests", to_run.len()), + &format!("{} doc examples for `{pkg_name}`", code_blocks.len()), )); - for (index, code_block) in to_run.iter().enumerate() { - let strategy = code_block.run_strategy(); - match self.run_single(code_block, index, strategy) { - Ok(res) => { - if res.status == TestStatus::Passed { - summary.passed += 1; - results.insert(code_block.id.clone(), res); - } else { + + for block in code_blocks { + let strategy = block.run_strategy(); + let total_in_item = *blocks_per_item.get(&block.id.item_full_path).unwrap_or(&1); + let display_name = block.id.display_name(total_in_item); + + match strategy { + RunStrategy::Ignore => { + summary.ignored += 1; + self.ui.print(TestResult::ignored(&display_name)); + } + _ => match self.run_single(block, strategy) { + Ok(res) => match res.status { + TestStatus::Passed => { + summary.passed += 1; + self.ui.print(TestResult::ok(&display_name)); + results.insert(block.id.clone(), res); + } + TestStatus::Failed => { + summary.failed += 1; + self.ui.print(TestResult::failed(&display_name)); + failed_names.push(display_name); + } + }, + Err(e) => { summary.failed += 1; + self.ui.print(TestResult::failed(&display_name)); + failed_names.push(display_name); + self.ui.error(format!("Error running example: {:#}", e)); } - } - Err(e) => { - summary.failed += 1; - self.ui - .error(format!("Error running example #{}: {:#}", index, e)); - } + }, + } + } + if !failed_names.is_empty() { + self.ui.print("\nfailures:"); + for display_name in &failed_names { + self.ui.print(format!(" {}", display_name)); } } + self.ui.print(NewLine::new()); + self.ui.print(summary.clone()); Ok((summary, results)) } - fn run_single( - &self, - code_block: &CodeBlock, - index: usize, - strategy: RunStrategy, - ) -> Result { - self.ui.print(Status::new( - "Running", - &format!("example #{} from `{}`", index, code_block.id.item_full_path), - )); - let ws = TestWorkspace::new(self.package_metadata, index, code_block)?; + fn run_single(&self, code_block: &CodeBlock, strategy: RunStrategy) -> Result { + let ws = TestWorkspace::new(self.package_metadata, code_block.id.block_index, code_block)?; let (actual, print_output, program_output) = self.run_single_inner(&ws, strategy)?; let expected = code_block.expected_outcome(); let status = if actual == expected { @@ -142,16 +187,6 @@ impl<'a> TestRunner<'a> { } else { TestStatus::Failed }; - match status { - TestStatus::Passed => self.ui.print(Status::new( - "Passed", - &format!("example #{} from `{}`", index, ws.item_full_path()), - )), - TestStatus::Failed => self.ui.print(Status::new( - "Failed", - &format!("example #{} from `{}`", index, ws.item_full_path()), - )), - } Ok(ExecutionResult { outcome: actual, @@ -185,7 +220,7 @@ impl<'a> TestRunner<'a> { return Ok((ExecutionOutcome::BuildSuccess, String::new(), String::new())); } - let output_dir = target_dir.join("execute").join(&ws.package_name()); + let output_dir = target_dir.join("execute").join(ws.package_name()); create_output_dir(output_dir.as_std_path())?; let (output_dir, execution_id) = incremental_create_execution_output_dir(&output_dir)?; diff --git a/extensions/scarb-doc/src/doc_test/workspace.rs b/extensions/scarb-doc/src/doc_test/workspace.rs index 4ad75d01a..4d73b39af 100644 --- a/extensions/scarb-doc/src/doc_test/workspace.rs +++ b/extensions/scarb-doc/src/doc_test/workspace.rs @@ -9,11 +9,10 @@ use std::fmt::Write; use std::fs; use tempfile::{TempDir, tempdir}; -pub struct TestWorkspace { +pub(crate) struct TestWorkspace { _temp_dir: TempDir, root: Utf8PathBuf, package_name: String, - item_full_path: String, } impl TestWorkspace { @@ -28,7 +27,6 @@ impl TestWorkspace { _temp_dir: temp_dir, root, package_name, - item_full_path: code_block.id.item_full_path.clone(), }; workspace.write_manifest(metadata)?; workspace.write_src(&code_block.content, &metadata.name)?; @@ -48,10 +46,6 @@ impl TestWorkspace { &self.package_name } - pub fn item_full_path(&self) -> &str { - &self.item_full_path - } - fn write_manifest(&self, metadata: &PackageMetadata) -> Result<()> { let package_dir = metadata .manifest_path @@ -59,7 +53,7 @@ impl TestWorkspace { .context("package manifest path has no parent directory")?; let dep = &metadata.name; - let dep_path = format!("\"{}\"", package_dir); + let dep_path = format!("{}", package_dir); let name = &self.package_name; let edition = edition_variant(Edition::latest()); @@ -70,7 +64,7 @@ impl TestWorkspace { edition = "{edition}" [dependencies] - {dep} = {{ path = {dep_path} }} + {dep} = {{ path = "{dep_path}" }} cairo_execute = "{CAIRO_VERSION}" [cairo] @@ -79,7 +73,7 @@ impl TestWorkspace { [executable] "# }; - fs::write(&self.manifest_path(), manifest).context("failed to write manifest")?; + fs::write(self.manifest_path(), manifest).context("failed to write manifest")?; Ok(()) } @@ -87,17 +81,28 @@ impl TestWorkspace { let src_dir = self.root().join("src"); fs::create_dir_all(&src_dir).context("failed to create src directory")?; - let mut body = String::with_capacity(content.len() + content.lines().count() * 5); - for line in content.lines() { - writeln!(body, " {}", line)?; - } + // TODO: This check is flawed and can be improved. + let has_main_fn = content.lines().any(|line| { + line.trim_start().starts_with("fn main()") + || line.trim_start().starts_with("pub fn main()") + }); + + let body = if has_main_fn { + content.to_string() + } else { + let mut body = String::with_capacity(content.len() + content.lines().count() * 5); + writeln!(body, "fn main() {{")?; + for line in content.lines() { + writeln!(body, " {}", line)?; + } + writeln!(body, "}}")?; + body + }; let lib_cairo = formatdoc! {r#" use {package_name}::*; #[executable] - fn main() {{ {body} - }} "#}; fs::write(src_dir.join("lib.cairo"), lib_cairo).context("failed to write lib.cairo")?; Ok(()) diff --git a/extensions/scarb-doc/src/main.rs b/extensions/scarb-doc/src/main.rs index 762f85525..08957a3b2 100644 --- a/extensions/scarb-doc/src/main.rs +++ b/extensions/scarb-doc/src/main.rs @@ -86,8 +86,9 @@ fn run_doc_tests(package: &PackageInformation, ui: &Ui) -> Result i32 { + /// add(-1, 1) + /// } + /// ``` + pub fn add(a: i32, b: i32) -> i32 { + a + b + } + + /// Main function that cairo runs as a binary entrypoint. + fn main() { + println!("hello_world"); + } diff --git a/extensions/scarb-doc/tests/data/runnable_examples_multiple_per_item/book.toml b/extensions/scarb-doc/tests/data/runnable_examples_multiple_per_item/book.toml new file mode 100644 index 000000000..cbcfa69da --- /dev/null +++ b/extensions/scarb-doc/tests/data/runnable_examples_multiple_per_item/book.toml @@ -0,0 +1,19 @@ +[book] +authors = [""] +language = "en" +multilingual = false +src = "src" +title = "hello_world - Cairo" + +[output.html] +no-section-label = true + +[output.html.playground] +runnable = false + +[output.html.fold] +enable = true +level = 0 + +[output.html.code.hidelines] +cairo = "#" diff --git a/extensions/scarb-doc/tests/data/runnable_examples_multiple_per_item/src/SUMMARY.md b/extensions/scarb-doc/tests/data/runnable_examples_multiple_per_item/src/SUMMARY.md new file mode 100644 index 000000000..9fc9b6790 --- /dev/null +++ b/extensions/scarb-doc/tests/data/runnable_examples_multiple_per_item/src/SUMMARY.md @@ -0,0 +1,3 @@ +- [hello_world](./hello_world.md) + - [Free functions](./hello_world-free_functions.md) + - [add](./hello_world-add.md) diff --git a/extensions/scarb-doc/tests/data/runnable_examples_multiple_per_item/src/hello_world-add.md b/extensions/scarb-doc/tests/data/runnable_examples_multiple_per_item/src/hello_world-add.md new file mode 100644 index 000000000..b3876f0e8 --- /dev/null +++ b/extensions/scarb-doc/tests/data/runnable_examples_multiple_per_item/src/hello_world-add.md @@ -0,0 +1,30 @@ +# add + +Function that returns the sum of two integers. +Example 1: +```cairo, runnable +let x = add(2, 3); +println!("{}", x); +``` + +Output: +``` +5 +``` + +Example 2: +```cairo, runnable +fn main() -> i32 { + add(-1, 1) +} +``` +Result: +``` +0 +``` + + +Fully qualified path: [hello_world](./hello_world.md)::[add](./hello_world-add.md) + +
pub fn add(a: i32, b: i32) -> i32
+ diff --git a/extensions/scarb-doc/tests/data/runnable_examples_multiple_per_item/src/hello_world-free_functions.md b/extensions/scarb-doc/tests/data/runnable_examples_multiple_per_item/src/hello_world-free_functions.md new file mode 100644 index 000000000..237a2f634 --- /dev/null +++ b/extensions/scarb-doc/tests/data/runnable_examples_multiple_per_item/src/hello_world-free_functions.md @@ -0,0 +1,6 @@ + +## [Free functions](./hello_world-free_functions.md) + +| | | +|:---|:---| +| [add](./hello_world-add.md) | Function that returns the sum of two integers. Example 1:... | diff --git a/extensions/scarb-doc/tests/data/runnable_examples_multiple_per_item/src/hello_world.md b/extensions/scarb-doc/tests/data/runnable_examples_multiple_per_item/src/hello_world.md new file mode 100644 index 000000000..8d89eb0fb --- /dev/null +++ b/extensions/scarb-doc/tests/data/runnable_examples_multiple_per_item/src/hello_world.md @@ -0,0 +1,10 @@ +# hello_world + +Fully qualified path: [hello_world](./hello_world.md) + + +## [Free functions](./hello_world-free_functions.md) + +| | | +|:---|:---| +| [add](./hello_world-add.md) | Function that returns the sum of two integers. Example 1:... | diff --git a/extensions/scarb-doc/tests/runnable_examples.rs b/extensions/scarb-doc/tests/runnable_examples.rs index c4afc260d..fc2ffe8d8 100644 --- a/extensions/scarb-doc/tests/runnable_examples.rs +++ b/extensions/scarb-doc/tests/runnable_examples.rs @@ -1,5 +1,5 @@ use assert_fs::TempDir; -use indoc::formatdoc; +use indoc::{formatdoc, indoc}; use scarb_test_support::command::Scarb; // use scarb_test_support::filesystem::dump_temp; use scarb_test_support::project_builder::ProjectBuilder; @@ -7,7 +7,11 @@ mod markdown_target; use markdown_target::MarkdownTargetChecker; const CODE_WITH_RUNNABLE_CODE_BLOCKS: &str = include_str!("code/code_12.cairo"); +const CODE_WITH_COMPILE_ERROR: &str = include_str!("code/code_13.cairo"); +const CODE_WITH_RUNTIME_ERROR: &str = include_str!("code/code_14.cairo"); +const CODE_WITH_MULTIPLE_CODE_BLOCKS_PER_ITEM: &str = include_str!("code/code_15.cairo"); const EXPECTED_WITH_EMBEDDINGS_PATH: &str = "tests/data/runnable_examples"; +const EXPECTED_MULTIPLE_PER_ITEM_PATH: &str = "tests/data/runnable_examples_multiple_per_item"; #[test] fn supports_runnable_examples() { @@ -25,16 +29,18 @@ fn supports_runnable_examples() { .assert() .success() .stdout_eq(formatdoc! {r#" - [..] Found 3 doc tests; 2 ignored - [..] Running 1 doc tests - [..] Running example #0 from `hello_world::foo_bar` + [..] Running 3 doc examples for `hello_world` + test hello_world::bar ... ignored + test hello_world::foo ... ignored [..] Compiling hello_world_example_0 v0.1.0 ([..]) [..] Finished `dev` profile target(s) in [..] [..] Executing hello_world_example_0 foo bar Saving output to: target/execute/hello_world_example_0/execution1 - [..] Passed example #0 from `hello_world::foo_bar` + test hello_world::foo_bar ... ok + + test result: ok. 1 passed; 0 failed; 2 ignored Saving output to: target/doc/hello_world Saving build output to: target/doc/hello_world/book @@ -50,3 +56,114 @@ fn supports_runnable_examples() { .expected(EXPECTED_WITH_EMBEDDINGS_PATH) .assert_all_files_match(); } + +#[test] +fn supports_runnable_examples_multiple_per_item() { + let t = TempDir::new().unwrap(); + ProjectBuilder::start() + .name("hello_world") + .lib_cairo(CODE_WITH_MULTIPLE_CODE_BLOCKS_PER_ITEM) + .build(&t); + + Scarb::quick_command() + .arg("doc") + .args(["--output-format", "markdown"]) + .arg("--build") + .current_dir(&t) + .assert() + .success() + .stdout_eq(formatdoc! {r#" + [..] Running 2 doc examples for `hello_world` + [..] Compiling hello_world_example_0 v0.1.0 ([..]) + [..] Finished `dev` profile target(s) in [..] + [..] Executing hello_world_example_0 + 5 + Saving output to: target/execute/hello_world_example_0/execution1 + test hello_world::add (example 0) ... ok + [..] Compiling hello_world_example_1 v0.1.0 ([..]) + [..] Finished `dev` profile target(s) in [..] + [..] Executing hello_world_example_1 + Saving output to: target/execute/hello_world_example_1/execution1 + test hello_world::add (example 1) ... ok + + test result: ok. 2 passed; 0 failed; 0 ignored + Saving output to: target/doc/hello_world + Saving build output to: target/doc/hello_world/book + + Run the following to see the results:[..] + `mdbook serve target/doc/hello_world` + + Or open the following in your browser:[..] + `[..]/target/doc/hello_world/book/index.html` + "#}); + + MarkdownTargetChecker::lenient() + .actual(t.path().join("target/doc/hello_world").to_str().unwrap()) + .expected(EXPECTED_MULTIPLE_PER_ITEM_PATH) + .assert_all_files_match(); +} + +#[test] +fn runnable_example_fails_at_compile_time() { + let t = TempDir::new().unwrap(); + ProjectBuilder::start() + .name("hello_world") + .lib_cairo(CODE_WITH_COMPILE_ERROR) + .build(&t); + + Scarb::quick_command() + .arg("doc") + .args(["--output-format", "markdown"]) + .arg("--build") + .current_dir(&t) + .assert() + .failure() + .stdout_eq(indoc! {r#" + [..] Running 1 doc examples for `hello_world` + [..] Compiling hello_world_example_0 v0.1.0 ([..]) + error[E0006]: Function not found. + --> [..]lib.cairo[..] + undefined(); + ^^^^^^^^^ + + error: could not compile `hello_world_example_0` due to 1 previous error + test hello_world::foo ... FAILED + + failures: + hello_world::foo + + test result: FAILED. 0 passed; 1 failed; 0 ignored + error: doc tests failed + "#}); +} + +#[test] +fn runnable_example_fails_at_runtime() { + let t = TempDir::new().unwrap(); + ProjectBuilder::start() + .name("hello_world") + .lib_cairo(CODE_WITH_RUNTIME_ERROR) + .build(&t); + + Scarb::quick_command() + .arg("doc") + .args(["--output-format", "markdown"]) + .arg("--build") + .current_dir(&t) + .assert() + .failure() + .stdout_eq(indoc! {r#" + [..] Running 1 doc examples for `hello_world` + [..] Compiling hello_world_example_0 v0.1.0 ([..]) + [..] Finished `dev` profile target(s) in [..] + [..] Executing hello_world_example_0 + error: Panicked with [..] + test hello_world::foo ... FAILED + + failures: + hello_world::foo + + test result: FAILED. 0 passed; 1 failed; 0 ignored + error: doc tests failed + "#}); +} diff --git a/utils/scarb-ui/src/components/mod.rs b/utils/scarb-ui/src/components/mod.rs index 7c90955a6..78f778c0b 100644 --- a/utils/scarb-ui/src/components/mod.rs +++ b/utils/scarb-ui/src/components/mod.rs @@ -5,6 +5,7 @@ pub use machine::*; pub use new_line::*; pub use spinner::*; pub use status::*; +pub use test_result::*; pub use typed::*; pub use value::*; @@ -12,5 +13,6 @@ mod machine; mod new_line; mod spinner; mod status; +mod test_result; mod typed; mod value; diff --git a/utils/scarb-ui/src/components/test_result.rs b/utils/scarb-ui/src/components/test_result.rs new file mode 100644 index 000000000..1c9846f63 --- /dev/null +++ b/utils/scarb-ui/src/components/test_result.rs @@ -0,0 +1,101 @@ +use console::Style; +use serde::{Serialize, Serializer}; + +use crate::Message; + +/// Result of a single test execution. +/// +/// Displays as `test {name} ... {status}` where `status` is colored: +/// - `ok` in green +/// - `FAILED` in red +/// - `ignored` in yellow +#[derive(Serialize)] +// TODO: move this to `scarb-doc` +pub struct TestResult<'a> { + name: &'a str, + #[serde(skip)] + status: TestResultStatus, +} + +/// Status of a test result. +#[derive(Clone, Copy)] +pub enum TestResultStatus { + /// Test passed. + Ok, + /// Test failed. + Failed, + /// Test was ignored. + Ignored, +} + +impl Serialize for TestResultStatus { + fn serialize(&self, ser: S) -> Result { + ser.serialize_str(self.as_str()) + } +} + +impl TestResultStatus { + fn as_str(&self) -> &'static str { + match self { + TestResultStatus::Ok => "ok", + TestResultStatus::Failed => "FAILED", + TestResultStatus::Ignored => "ignored", + } + } + + fn style(&self) -> Style { + match self { + TestResultStatus::Ok => Style::new().green(), + TestResultStatus::Failed => Style::new().red(), + TestResultStatus::Ignored => Style::new().yellow(), + } + } +} + +impl<'a> TestResult<'a> { + /// Creates a new [`TestResult`]. + pub fn new(name: &'a str, status: TestResultStatus) -> Self { + Self { name, status } + } + + /// Creates a new `ok` test result. + pub fn ok(name: &'a str) -> Self { + Self::new(name, TestResultStatus::Ok) + } + + /// Creates a new `FAILED` test result. + pub fn failed(name: &'a str) -> Self { + Self::new(name, TestResultStatus::Failed) + } + + /// Creates a new `ignored` test result. + pub fn ignored(name: &'a str) -> Self { + Self::new(name, TestResultStatus::Ignored) + } +} + +impl Message for TestResult<'_> { + fn text(self) -> String { + format!( + "test {} ... {}", + self.name, + self.status.style().apply_to(self.status.as_str()) + ) + } + + fn structured(self, ser: S) -> Result { + TestResultJson { + r#type: "test_result", + name: self.name, + status: self.status, + } + .serialize(ser) + } +} + +#[derive(Serialize)] +struct TestResultJson<'a> { + r#type: &'static str, + name: &'a str, + status: TestResultStatus, +} From e76d4a26b78548415632ac2f42586e2a01eff36e Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Tue, 2 Dec 2025 18:51:57 +0300 Subject: [PATCH 22/32] post-rebase fix --- .../scarb-doc/src/doc_test/code_blocks.rs | 13 +- .../src/docs_generation/markdown/context.rs | 4 +- .../src/docs_generation/markdown/traits.rs | 16 +- extensions/scarb-doc/src/types/item_data.rs | 8 +- extensions/scarb-doc/src/types/module_type.rs | 4 +- extensions/scarb-doc/src/types/other_types.rs | 217 ++++++++---------- 6 files changed, 125 insertions(+), 137 deletions(-) diff --git a/extensions/scarb-doc/src/doc_test/code_blocks.rs b/extensions/scarb-doc/src/doc_test/code_blocks.rs index 79c7b057c..534ebd127 100644 --- a/extensions/scarb-doc/src/doc_test/code_blocks.rs +++ b/extensions/scarb-doc/src/doc_test/code_blocks.rs @@ -1,7 +1,7 @@ use crate::doc_test::runner::{ExecutionOutcome, RunStrategy}; +use crate::docs_generation::markdown::traits::WithItemDataCommon; use crate::types::crate_type::Crate; use crate::types::module_type::Module; -use crate::types::other_types::ItemData; use cairo_lang_doc::parser::DocumentationCommentToken; use std::collections::HashMap; use std::str::from_utf8; @@ -156,16 +156,19 @@ pub fn count_blocks_per_item(code_blocks: &[CodeBlock]) -> HashMap, runnable_code_blocks: &mut Vec) { - for item_data in module.get_all_item_ids().values() { + for &item_data in module.get_all_item_ids().values() { collect_from_item_data(item_data, runnable_code_blocks); } - for item_data in module.pub_uses.get_all_item_ids().values() { + for &item_data in module.pub_uses.get_all_item_ids().values() { collect_from_item_data(item_data, runnable_code_blocks); } } -fn collect_from_item_data(item_data: &ItemData<'_>, runnable_code_blocks: &mut Vec) { - for block in &item_data.code_blocks { +fn collect_from_item_data( + item_data: &dyn WithItemDataCommon, + runnable_code_blocks: &mut Vec, +) { + for block in &item_data.code_blocks() { runnable_code_blocks.push(block.clone()); } } diff --git a/extensions/scarb-doc/src/docs_generation/markdown/context.rs b/extensions/scarb-doc/src/docs_generation/markdown/context.rs index 53a353599..e5b174f18 100644 --- a/extensions/scarb-doc/src/docs_generation/markdown/context.rs +++ b/extensions/scarb-doc/src/docs_generation/markdown/context.rs @@ -1,7 +1,7 @@ use crate::doc_test::runner::ExecutionResults; use crate::docs_generation::common::{OutputFilesExtension, SummaryIndexMap}; use crate::docs_generation::markdown::SUMMARY_FILENAME; -use crate::docs_generation::markdown::traits::WithPath; +use crate::docs_generation::markdown::traits::WithItemDataCommon; use crate::location_links::DocLocationLink; use crate::types::crate_type::Crate; use cairo_lang_defs::ids::{ImplItemId, LookupItemId, TraitItemId}; @@ -10,7 +10,7 @@ use cairo_lang_doc::parser::CommentLinkToken; use itertools::Itertools; use std::collections::HashMap; -pub type IncludedItems<'a, 'db> = HashMap, &'a dyn WithPath>; +pub type IncludedItems<'a, 'db> = HashMap, &'a dyn WithItemDataCommon>; pub struct MarkdownGenerationContext<'a, 'db> { included_items: IncludedItems<'a, 'db>, diff --git a/extensions/scarb-doc/src/docs_generation/markdown/traits.rs b/extensions/scarb-doc/src/docs_generation/markdown/traits.rs index 441acb220..071d495e6 100644 --- a/extensions/scarb-doc/src/docs_generation/markdown/traits.rs +++ b/extensions/scarb-doc/src/docs_generation/markdown/traits.rs @@ -1,4 +1,5 @@ use super::context::MarkdownGenerationContext; +use crate::doc_test::code_blocks::CodeBlock; use crate::docs_generation::markdown::{ BASE_MODULE_CHAPTER_PREFIX, GROUP_CHAPTER_PREFIX, SHORT_DOCUMENTATION_AVOID_PREFIXES, SHORT_DOCUMENTATION_LEN, @@ -888,17 +889,18 @@ fn get_full_subitem_path( } } -pub trait WithPath { +pub trait WithItemDataCommon { fn name(&self) -> &str; fn full_path(&self) -> String; fn parent_full_path(&self) -> Option; + fn code_blocks(&self) -> Vec; } pub trait WithItemData { fn item_data(&self) -> &ItemData<'_>; } -impl WithPath for T { +impl WithItemDataCommon for T { fn name(&self) -> &str { self.item_data().name.as_str() } @@ -910,6 +912,9 @@ impl WithPath for T { fn parent_full_path(&self) -> Option { self.item_data().parent_full_path.clone() } + fn code_blocks(&self) -> Vec { + self.item_data().code_blocks.clone() + } } impl<'db> WithItemData for ItemData<'db> { @@ -918,8 +923,8 @@ impl<'db> WithItemData for ItemData<'db> { } } -// Allow SubItemData to be used wherever a WithPath is expected without converting into ItemData. -impl<'db> WithPath for SubItemData<'db> { +/// Allow SubItemData to be used wherever a [`WithItemDataCommon`] is expected without converting into ItemData. +impl<'db> WithItemDataCommon for SubItemData<'db> { fn name(&self) -> &str { self.name.as_str() } @@ -929,4 +934,7 @@ impl<'db> WithPath for SubItemData<'db> { fn parent_full_path(&self) -> Option { self.parent_full_path.clone() } + fn code_blocks(&self) -> Vec { + self.code_blocks.clone() + } } diff --git a/extensions/scarb-doc/src/types/item_data.rs b/extensions/scarb-doc/src/types/item_data.rs index 59d959731..f6b1038d2 100644 --- a/extensions/scarb-doc/src/types/item_data.rs +++ b/extensions/scarb-doc/src/types/item_data.rs @@ -1,5 +1,6 @@ use crate::attributes::find_groups_from_attributes; use crate::db::ScarbDocDatabase; +use crate::doc_test::code_blocks::{CodeBlock, collect_code_blocks_from_tokens}; use crate::location_links::DocLocationLink; use crate::types::other_types::doc_full_path; use cairo_lang_defs::ids::{ModuleId, TopLevelLanguageElementId}; @@ -10,7 +11,6 @@ use cairo_lang_filesystem::ids::CrateId; use serde::Serialize; use serde::Serializer; use std::fmt::Debug; -use crate::code_blocks::{collect_code_blocks, collect_code_blocks_from_tokens, CodeBlock}; #[derive(Debug, Serialize, Clone)] pub struct ItemData<'db> { @@ -120,6 +120,8 @@ pub struct SubItemData<'db> { pub signature: Option, pub full_path: String, #[serde(skip_serializing)] + pub code_blocks: Vec, + #[serde(skip_serializing)] pub doc_location_links: Vec, #[serde(skip_serializing)] pub group: Option, @@ -134,8 +136,7 @@ impl<'db> From> for ItemData<'db> { doc: val.doc, signature: val.signature, full_path: val.full_path, - // TODO: fix this - code_blocks: Default::default(), + code_blocks: val.code_blocks, doc_location_links: val.doc_location_links, group: val.group, } @@ -152,6 +153,7 @@ impl<'db> From> for SubItemData<'db> { signature: val.signature, full_path: val.full_path, doc_location_links: val.doc_location_links, + code_blocks: val.code_blocks, group: val.group, } } diff --git a/extensions/scarb-doc/src/types/module_type.rs b/extensions/scarb-doc/src/types/module_type.rs index 8dc92d28d..227df9bbc 100644 --- a/extensions/scarb-doc/src/types/module_type.rs +++ b/extensions/scarb-doc/src/types/module_type.rs @@ -241,8 +241,8 @@ impl<'db> ModulePubUses<'db> { self.use_macro_declarations.extend(use_macro_declarations); } - pub(crate) fn get_all_item_ids(&self) -> HashMap, &ItemData<'_>> { - let mut ids: HashMap = HashMap::default(); + pub fn get_all_item_ids<'a>(&'a self) -> IncludedItems<'a, 'db> { + let mut ids: IncludedItems<'a, 'db> = HashMap::default(); self.use_constants.iter().for_each(|item| { ids.insert(item.item_data.id, &item.item_data); diff --git a/extensions/scarb-doc/src/types/other_types.rs b/extensions/scarb-doc/src/types/other_types.rs index 5abfa6b96..49fd89357 100644 --- a/extensions/scarb-doc/src/types/other_types.rs +++ b/extensions/scarb-doc/src/types/other_types.rs @@ -1,10 +1,6 @@ -use anyhow::Result; - -use crate::attributes::find_groups_from_attributes; use crate::db::ScarbDocDatabase; -use crate::doc_test::code_blocks::{CodeBlock, collect_code_blocks_from_tokens}; use crate::docs_generation::markdown::context::IncludedItems; -use crate::docs_generation::markdown::traits::WithPath; +use crate::docs_generation::markdown::traits::WithItemDataCommon; use crate::types::item_data::{ItemData, SubItemData}; use cairo_lang_defs::ids::NamedLanguageElementId; use cairo_lang_defs::ids::{ @@ -22,25 +18,7 @@ use cairo_lang_syntax::node::ast; use serde::Serialize; use std::collections::HashMap; -#[derive(Debug, Serialize, Clone)] -pub struct ItemData<'db> { - #[serde(skip_serializing)] - pub id: DocumentableItemId<'db>, - #[serde(skip_serializing)] - pub parent_full_path: Option, - pub name: String, - #[serde(serialize_with = "documentation_serializer")] - pub doc: Option>>, - pub signature: Option, - pub full_path: String, - #[serde(skip_serializing)] - pub code_blocks: Vec, - #[serde(skip_serializing)] - pub doc_location_links: Vec, - pub group: Option, -} - -/// Mimics the [`TopLevelLanguageElementId::full_path`] but skips the macro modules. +/// Mimics the [`cairo_lang_defs::ids::TopLevelLanguageElementId::full_path`] but skips the macro modules. /// If not omitted, the path would look like, for example, /// `hello::define_fn_outter!(func_macro_fn_outter);::expose! {\n\t\t\tpub fn func_macro_fn_outter() -> felt252 { \n\t\t\t\tprintln!(\"hello world\");\n\t\t\t\t10 }\n\t\t}::func_macro_fn_outter` pub fn doc_full_path(module_id: &ModuleId, db: &ScarbDocDatabase) -> String { @@ -57,102 +35,6 @@ pub fn doc_full_path(module_id: &ModuleId, db: &ScarbDocDatabase) -> String { } } -impl<'db> ItemData<'db> { - pub fn new( - db: &'db ScarbDocDatabase, - id: impl TopLevelLanguageElementId<'db>, - documentable_item_id: DocumentableItemId<'db>, - parent_full_path: String, - ) -> Self { - let (signature, doc_location_links) = - db.get_item_signature_with_links(documentable_item_id); - let doc_location_links = doc_location_links - .iter() - .map(|link| DocLocationLink::new(link.start, link.end, link.item_id, db)) - .collect::>(); - let group = find_groups_from_attributes(db, &id); - let full_path = id.full_path(db); - let doc = db.get_item_documentation_as_tokens(documentable_item_id); - let code_blocks = collect_code_blocks_from_tokens(&doc, &full_path); - - Self { - id: documentable_item_id, - name: id.name(db).to_string(db), - doc, - signature, - full_path: format!("{}::{}", parent_full_path, id.name(db).long(db)), - parent_full_path: Some(parent_full_path), - code_blocks, - doc_location_links, - group, - } - } - - pub fn new_without_signature( - db: &'db ScarbDocDatabase, - id: impl TopLevelLanguageElementId<'db>, - documentable_item_id: DocumentableItemId<'db>, - ) -> Self { - let full_path = format!( - "{}::{}", - doc_full_path(&id.parent_module(db), db), - id.name(db).long(db) - ); - let doc = db.get_item_documentation_as_tokens(documentable_item_id); - let code_blocks = collect_code_blocks_from_tokens(&doc, &full_path); - - Self { - id: documentable_item_id, - name: id.name(db).to_string(db), - doc, - signature: None, - full_path, - parent_full_path: Some(id.parent_module(db).full_path(db)), - code_blocks, - doc_location_links: vec![], - group: find_groups_from_attributes(db, &id), - } - } - - pub fn new_crate(db: &'db ScarbDocDatabase, id: CrateId<'db>) -> Self { - let documentable_id = DocumentableItemId::Crate(id); - let full_path = ModuleId::CrateRoot(id).full_path(db); - let doc = db.get_item_documentation_as_tokens(documentable_id); - let code_blocks = collect_code_blocks_from_tokens(&doc, &full_path); - - Self { - id: documentable_id, - name: id.long(db).name().to_string(db), - doc, - signature: None, - full_path, - parent_full_path: None, - code_blocks, - doc_location_links: vec![], - group: None, - } - } -} - -fn documentation_serializer( - docs: &Option>, - serializer: S, -) -> Result -where - S: Serializer, -{ - match docs { - Some(doc_vec) => { - let combined = doc_vec - .iter() - .map(|dct| dct.to_string()) - .collect::>(); - serializer.serialize_str(&combined.join("")) - } - None => serializer.serialize_none(), - } -} - #[derive(Serialize, Clone)] pub struct Constant<'db> { #[serde(skip)] @@ -207,6 +89,94 @@ impl<'db> FreeFunction<'db> { } } +#[derive(Serialize, Clone)] +pub struct Struct<'db> { + #[serde(skip)] + pub id: StructId<'db>, + #[serde(skip)] + pub node: ast::ItemStructPtr<'db>, + + pub members: Vec>, + + pub item_data: ItemData<'db>, +} + +impl<'db> Struct<'db> { + pub fn new( + db: &'db ScarbDocDatabase, + id: StructId<'db>, + include_private_items: bool, + ) -> Maybe { + let members = db.struct_members(id)?; + + let item_data = ItemData::new( + db, + id, + LookupItemId::ModuleItem(ModuleItemId::Struct(id)).into(), + doc_full_path(&id.parent_module(db), db), + ); + let members = members + .iter() + .filter_map(|(_, semantic_member)| { + let visible = matches!(semantic_member.visibility, Visibility::Public); + let syntax_node = &semantic_member.id.stable_location(db).syntax_node(db); + if (include_private_items || visible) && !is_doc_hidden_attr(db, syntax_node) { + Some(Ok(Member::new(db, semantic_member.id))) + } else { + None + } + }) + .collect::>>()?; + + let node = id.stable_ptr(db); + Ok(Self { + id, + node, + members, + item_data, + }) + } + + pub fn get_all_item_ids<'a>(&'a self) -> IncludedItems<'a, 'db> { + self.members + .iter() + .map(|item| { + ( + item.item_data.id, + &item.item_data as &dyn WithItemDataCommon, + ) + }) + .collect() + } +} + +#[derive(Serialize, Clone)] +pub struct Member<'db> { + #[serde(skip)] + pub id: MemberId<'db>, + #[serde(skip)] + pub node: ast::MemberPtr<'db>, + + pub item_data: SubItemData<'db>, +} + +impl<'db> Member<'db> { + pub fn new(db: &'db ScarbDocDatabase, id: MemberId<'db>) -> Self { + let node = id.stable_ptr(db); + + let parent_path = format!( + "{}::{}", + doc_full_path(&id.parent_module(db), db), + id.struct_id(db).name(db).to_string(db) + ); + Self { + id, + node, + item_data: ItemData::new(db, id, DocumentableItemId::Member(id), parent_path).into(), + } + } +} + #[derive(Serialize, Clone)] pub struct Enum<'db> { #[serde(skip)] @@ -246,7 +216,12 @@ impl<'db> Enum<'db> { pub fn get_all_item_ids<'a>(&'a self) -> IncludedItems<'a, 'db> { self.variants .iter() - .map(|item| (item.item_data.id, &item.item_data as &dyn WithPath)) + .map(|item| { + ( + item.item_data.id, + &item.item_data as &dyn WithItemDataCommon, + ) + }) .collect() } } From a54f1de5682760f5bb5283b19cdf28d82e816fed Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Tue, 2 Dec 2025 20:01:17 +0300 Subject: [PATCH 23/32] misc cleanup --- extensions/scarb-doc/src/doc_test.rs | 1 + .../scarb-doc/src/doc_test/code_blocks.rs | 16 +-- extensions/scarb-doc/src/doc_test/runner.rs | 18 ++-- .../scarb-doc/src/doc_test/workspace.rs | 2 +- utils/scarb-ui/src/components/mod.rs | 2 - utils/scarb-ui/src/components/test_result.rs | 101 ------------------ 6 files changed, 18 insertions(+), 122 deletions(-) delete mode 100644 utils/scarb-ui/src/components/test_result.rs diff --git a/extensions/scarb-doc/src/doc_test.rs b/extensions/scarb-doc/src/doc_test.rs index c56782f92..72a07008d 100644 --- a/extensions/scarb-doc/src/doc_test.rs +++ b/extensions/scarb-doc/src/doc_test.rs @@ -1,3 +1,4 @@ pub mod code_blocks; pub mod runner; +pub mod test_result; pub mod workspace; diff --git a/extensions/scarb-doc/src/doc_test/code_blocks.rs b/extensions/scarb-doc/src/doc_test/code_blocks.rs index 534ebd127..ab9eb0a4a 100644 --- a/extensions/scarb-doc/src/doc_test/code_blocks.rs +++ b/extensions/scarb-doc/src/doc_test/code_blocks.rs @@ -23,8 +23,7 @@ impl CodeBlockId { } } - // TODO: ideally, this should be replaced with logic that - // tracks the exact line of the code block in the source file. + // TODO: (#2888): Display exact code block location when running doc-tests pub fn display_name(&self, total_blocks_in_item: usize) -> String { if total_blocks_in_item <= 1 { self.item_full_path.clone() @@ -77,13 +76,11 @@ impl CodeBlock { } } - // TODO: default to Cairo unless specified otherwise? + // TODO: default to Cairo unless specified otherwise fn is_cairo(&self) -> bool { if self.attributes.contains(&CodeBlockAttribute::Cairo) { return true; } - // TODO: Assume unknown attributes imply non-Cairo code. - // !self.attributes.iter().any(|attr| matches!(attr, CodeBlockAttribute::Other(_))) false } @@ -91,7 +88,7 @@ impl CodeBlock { if self.attributes.contains(&CodeBlockAttribute::Ignore) { return RunStrategy::Ignore; } - // TODO: remove this later on + // TODO: drop the `runnable` attribute requirement; default to runnable for Cairo blocks if !self.is_cairo() || !self.attributes.contains(&CodeBlockAttribute::Runnable) { return RunStrategy::Ignore; } @@ -104,7 +101,6 @@ impl CodeBlock { } } - /// Returns the expected execution outcome based on attributes. pub fn expected_outcome(&self) -> ExecutionOutcome { if self.attributes.contains(&CodeBlockAttribute::Ignore) { return ExecutionOutcome::None; @@ -137,14 +133,12 @@ pub fn collect_code_blocks(crate_: &Crate<'_>) -> Vec { for module in &crate_.foreign_crates { collect_from_module(module, &mut runnable_code_blocks); } - // Sort to run deterministically runnable_code_blocks.sort_by_key(|block| block.id.clone()); runnable_code_blocks } -/// Counts the number of code blocks per documented item. -/// This is used to generate display names for code blocks, -/// allowing to distinguish between multiple code blocks in the same item. +/// Counts the number of code blocks per documented item. Used to generate display names +/// for code blocks, allowing to distinguish between multiple code blocks in the same item. /// /// Returns the mapping from `item_full_path` to the number of code blocks in that item. pub fn count_blocks_per_item(code_blocks: &[CodeBlock]) -> HashMap { diff --git a/extensions/scarb-doc/src/doc_test/runner.rs b/extensions/scarb-doc/src/doc_test/runner.rs index 93987967a..4fca500b3 100644 --- a/extensions/scarb-doc/src/doc_test/runner.rs +++ b/extensions/scarb-doc/src/doc_test/runner.rs @@ -1,4 +1,5 @@ use crate::doc_test::code_blocks::{CodeBlock, CodeBlockId, count_blocks_per_item}; +use crate::doc_test::test_result::TestResult; use crate::doc_test::workspace::TestWorkspace; use anyhow::Result; use console::Style; @@ -8,7 +9,7 @@ use scarb_execute_utils::{ incremental_create_execution_output_dir, }; use scarb_metadata::{PackageMetadata, ScarbCommand}; -use scarb_ui::components::{NewLine, Status, TestResult}; +use scarb_ui::components::{NewLine, Status}; use scarb_ui::{Message, Ui}; use serde::{Serialize, Serializer}; use std::collections::HashMap; @@ -25,7 +26,6 @@ pub struct ExecutionResult { } impl ExecutionResult { - /// Formats the execution result as markdown with code blocks. pub fn as_markdown(&self) -> String { let mut output = String::new(); if !self.print_output.is_empty() { @@ -64,14 +64,14 @@ impl TestSummary { impl Message for TestSummary { fn text(self) -> String { - let (status_word, style) = if self.failed == 0 { + let (result, style) = if self.is_ok() { ("ok", Style::new().green()) } else { ("FAILED", Style::new().red()) }; format!( "test result: {}. {} passed; {} failed; {} ignored", - style.apply_to(status_word), + style.apply_to(result), self.passed, self.failed, self.ignored @@ -105,9 +105,14 @@ pub enum RunStrategy { Execute, } -/// A runner for executing examples (code blocks) found in documentation. -/// Uses `scarb execute` and runs code blocks in isolated temporary workspaces. +/// A runner for executing ([`CodeBlock`]) examples found in documentation. +/// Uses the target package as a dependency and runs each code block in an isolated temporary workspace. +/// Relies on `scarb build` and `scarb execute` commands to build and run the examples, +/// based on the requested [`RunStrategy`] for the given code block. +/// +/// Note: it is expected examples (`code_blocks`) that this runner executes only depend on the target package and standard libraries. pub struct TestRunner<'a> { + /// Metadata of the target package whose documentation is being tested. package_metadata: &'a PackageMetadata, ui: Ui, } @@ -120,7 +125,6 @@ impl<'a> TestRunner<'a> { } } - /// Run all the code code blocks for a given package. pub fn run_all(&self, code_blocks: &[CodeBlock]) -> Result<(TestSummary, ExecutionResults)> { let pkg_name = &self.package_metadata.name; diff --git a/extensions/scarb-doc/src/doc_test/workspace.rs b/extensions/scarb-doc/src/doc_test/workspace.rs index 4d73b39af..22a9e3f15 100644 --- a/extensions/scarb-doc/src/doc_test/workspace.rs +++ b/extensions/scarb-doc/src/doc_test/workspace.rs @@ -81,7 +81,7 @@ impl TestWorkspace { let src_dir = self.root().join("src"); fs::create_dir_all(&src_dir).context("failed to create src directory")?; - // TODO: This check is flawed and can be improved. + // TODO: (#2889) Improve this logic to be more precise let has_main_fn = content.lines().any(|line| { line.trim_start().starts_with("fn main()") || line.trim_start().starts_with("pub fn main()") diff --git a/utils/scarb-ui/src/components/mod.rs b/utils/scarb-ui/src/components/mod.rs index 78f778c0b..7c90955a6 100644 --- a/utils/scarb-ui/src/components/mod.rs +++ b/utils/scarb-ui/src/components/mod.rs @@ -5,7 +5,6 @@ pub use machine::*; pub use new_line::*; pub use spinner::*; pub use status::*; -pub use test_result::*; pub use typed::*; pub use value::*; @@ -13,6 +12,5 @@ mod machine; mod new_line; mod spinner; mod status; -mod test_result; mod typed; mod value; diff --git a/utils/scarb-ui/src/components/test_result.rs b/utils/scarb-ui/src/components/test_result.rs deleted file mode 100644 index 1c9846f63..000000000 --- a/utils/scarb-ui/src/components/test_result.rs +++ /dev/null @@ -1,101 +0,0 @@ -use console::Style; -use serde::{Serialize, Serializer}; - -use crate::Message; - -/// Result of a single test execution. -/// -/// Displays as `test {name} ... {status}` where `status` is colored: -/// - `ok` in green -/// - `FAILED` in red -/// - `ignored` in yellow -#[derive(Serialize)] -// TODO: move this to `scarb-doc` -pub struct TestResult<'a> { - name: &'a str, - #[serde(skip)] - status: TestResultStatus, -} - -/// Status of a test result. -#[derive(Clone, Copy)] -pub enum TestResultStatus { - /// Test passed. - Ok, - /// Test failed. - Failed, - /// Test was ignored. - Ignored, -} - -impl Serialize for TestResultStatus { - fn serialize(&self, ser: S) -> Result { - ser.serialize_str(self.as_str()) - } -} - -impl TestResultStatus { - fn as_str(&self) -> &'static str { - match self { - TestResultStatus::Ok => "ok", - TestResultStatus::Failed => "FAILED", - TestResultStatus::Ignored => "ignored", - } - } - - fn style(&self) -> Style { - match self { - TestResultStatus::Ok => Style::new().green(), - TestResultStatus::Failed => Style::new().red(), - TestResultStatus::Ignored => Style::new().yellow(), - } - } -} - -impl<'a> TestResult<'a> { - /// Creates a new [`TestResult`]. - pub fn new(name: &'a str, status: TestResultStatus) -> Self { - Self { name, status } - } - - /// Creates a new `ok` test result. - pub fn ok(name: &'a str) -> Self { - Self::new(name, TestResultStatus::Ok) - } - - /// Creates a new `FAILED` test result. - pub fn failed(name: &'a str) -> Self { - Self::new(name, TestResultStatus::Failed) - } - - /// Creates a new `ignored` test result. - pub fn ignored(name: &'a str) -> Self { - Self::new(name, TestResultStatus::Ignored) - } -} - -impl Message for TestResult<'_> { - fn text(self) -> String { - format!( - "test {} ... {}", - self.name, - self.status.style().apply_to(self.status.as_str()) - ) - } - - fn structured(self, ser: S) -> Result { - TestResultJson { - r#type: "test_result", - name: self.name, - status: self.status, - } - .serialize(ser) - } -} - -#[derive(Serialize)] -struct TestResultJson<'a> { - r#type: &'static str, - name: &'a str, - status: TestResultStatus, -} From ac7e1897cf963c9bdeb35fe523675690768baeb0 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Tue, 2 Dec 2025 20:39:39 +0300 Subject: [PATCH 24/32] misc cleanup --- extensions/scarb-doc/src/doc_test.rs | 4 +- extensions/scarb-doc/src/doc_test/runner.rs | 29 +---- extensions/scarb-doc/src/doc_test/ui.rs | 113 ++++++++++++++++++++ 3 files changed, 119 insertions(+), 27 deletions(-) create mode 100644 extensions/scarb-doc/src/doc_test/ui.rs diff --git a/extensions/scarb-doc/src/doc_test.rs b/extensions/scarb-doc/src/doc_test.rs index 72a07008d..30e7cf7b2 100644 --- a/extensions/scarb-doc/src/doc_test.rs +++ b/extensions/scarb-doc/src/doc_test.rs @@ -1,4 +1,4 @@ pub mod code_blocks; pub mod runner; -pub mod test_result; -pub mod workspace; +mod ui; +mod workspace; diff --git a/extensions/scarb-doc/src/doc_test/runner.rs b/extensions/scarb-doc/src/doc_test/runner.rs index 4fca500b3..cc0f9539c 100644 --- a/extensions/scarb-doc/src/doc_test/runner.rs +++ b/extensions/scarb-doc/src/doc_test/runner.rs @@ -1,17 +1,16 @@ use crate::doc_test::code_blocks::{CodeBlock, CodeBlockId, count_blocks_per_item}; -use crate::doc_test::test_result::TestResult; +use crate::doc_test::ui::TestResult; use crate::doc_test::workspace::TestWorkspace; use anyhow::Result; -use console::Style; use create_output_dir::create_output_dir; use scarb_execute_utils::{ EXECUTE_PRINT_OUTPUT_FILENAME, EXECUTE_PROGRAM_OUTPUT_FILENAME, incremental_create_execution_output_dir, }; use scarb_metadata::{PackageMetadata, ScarbCommand}; +use scarb_ui::Ui; use scarb_ui::components::{NewLine, Status}; -use scarb_ui::{Message, Ui}; -use serde::{Serialize, Serializer}; +use serde::Serialize; use std::collections::HashMap; use std::fs; @@ -62,27 +61,6 @@ impl TestSummary { } } -impl Message for TestSummary { - fn text(self) -> String { - let (result, style) = if self.is_ok() { - ("ok", Style::new().green()) - } else { - ("FAILED", Style::new().red()) - }; - format!( - "test result: {}. {} passed; {} failed; {} ignored", - style.apply_to(result), - self.passed, - self.failed, - self.ignored - ) - } - - fn structured(self, ser: S) -> Result { - self.serialize(ser) - } -} - #[derive(Debug, Clone, PartialEq, Eq)] pub enum TestStatus { Passed, @@ -170,6 +148,7 @@ impl<'a> TestRunner<'a> { }, } } + // TODO: add struct with `impl Message` to display this if !failed_names.is_empty() { self.ui.print("\nfailures:"); for display_name in &failed_names { diff --git a/extensions/scarb-doc/src/doc_test/ui.rs b/extensions/scarb-doc/src/doc_test/ui.rs new file mode 100644 index 000000000..f5c5ab33c --- /dev/null +++ b/extensions/scarb-doc/src/doc_test/ui.rs @@ -0,0 +1,113 @@ +use crate::doc_test::runner::TestSummary; +use console::Style; +use scarb_ui::Message; +use serde::{Serialize, Serializer}; + +impl Message for TestSummary { + fn text(self) -> String { + let (result, style) = if self.is_ok() { + ("ok", Style::new().green()) + } else { + ("FAILED", Style::new().red()) + }; + format!( + "test result: {}. {} passed; {} failed; {} ignored", + style.apply_to(result), + self.passed, + self.failed, + self.ignored + ) + } + + fn structured(self, ser: S) -> anyhow::Result { + self.serialize(ser) + } +} + +/// Result of a single test execution. +/// +/// Displays as `test {name} ... {status}` where `status` is colored: +/// - `ok` in green +/// - `FAILED` in red +/// - `ignored` in yellow +#[derive(Serialize)] +pub struct TestResult<'a> { + name: &'a str, + #[serde(skip)] + status: TestResultStatus, +} + +#[derive(Clone, Copy)] +pub enum TestResultStatus { + Ok, + Failed, + Ignored, +} + +impl Serialize for TestResultStatus { + fn serialize(&self, ser: S) -> Result { + ser.serialize_str(self.as_str()) + } +} + +impl TestResultStatus { + fn as_str(&self) -> &'static str { + match self { + TestResultStatus::Ok => "ok", + TestResultStatus::Failed => "FAILED", + TestResultStatus::Ignored => "ignored", + } + } + + fn style(&self) -> Style { + match self { + TestResultStatus::Ok => Style::new().green(), + TestResultStatus::Failed => Style::new().red(), + TestResultStatus::Ignored => Style::new().yellow(), + } + } +} + +impl<'a> TestResult<'a> { + pub fn new(name: &'a str, status: TestResultStatus) -> Self { + Self { name, status } + } + + pub fn ok(name: &'a str) -> Self { + Self::new(name, TestResultStatus::Ok) + } + + pub fn failed(name: &'a str) -> Self { + Self::new(name, TestResultStatus::Failed) + } + + pub fn ignored(name: &'a str) -> Self { + Self::new(name, TestResultStatus::Ignored) + } +} + +impl Message for TestResult<'_> { + fn text(self) -> String { + format!( + "test {} ... {}", + self.name, + self.status.style().apply_to(self.status.as_str()) + ) + } + + fn structured(self, ser: S) -> Result { + TestResultJson { + r#type: "test_result", + name: self.name, + status: self.status, + } + .serialize(ser) + } +} + +#[derive(Serialize)] +struct TestResultJson<'a> { + r#type: &'static str, + name: &'a str, + status: TestResultStatus, +} From d1bda3ce5a9591610903005e70119a1d0a6e222b Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 3 Dec 2025 18:58:11 +0300 Subject: [PATCH 25/32] fix indexing of examples --- extensions/scarb-doc/src/doc_test/runner.rs | 36 ++++++++++--------- .../scarb-doc/tests/runnable_examples.rs | 28 +++++++-------- 2 files changed, 34 insertions(+), 30 deletions(-) diff --git a/extensions/scarb-doc/src/doc_test/runner.rs b/extensions/scarb-doc/src/doc_test/runner.rs index cc0f9539c..cf610f435 100644 --- a/extensions/scarb-doc/src/doc_test/runner.rs +++ b/extensions/scarb-doc/src/doc_test/runner.rs @@ -116,6 +116,7 @@ impl<'a> TestRunner<'a> { &format!("{} doc examples for `{pkg_name}`", code_blocks.len()), )); + let mut idx = 0; for block in code_blocks { let strategy = block.run_strategy(); let total_in_item = *blocks_per_item.get(&block.id.item_full_path).unwrap_or(&1); @@ -126,24 +127,27 @@ impl<'a> TestRunner<'a> { summary.ignored += 1; self.ui.print(TestResult::ignored(&display_name)); } - _ => match self.run_single(block, strategy) { - Ok(res) => match res.status { - TestStatus::Passed => { - summary.passed += 1; - self.ui.print(TestResult::ok(&display_name)); - results.insert(block.id.clone(), res); - } - TestStatus::Failed => { + _ => { + idx += 1; + match self.run_single(block, strategy, idx) { + Ok(res) => match res.status { + TestStatus::Passed => { + summary.passed += 1; + self.ui.print(TestResult::ok(&display_name)); + results.insert(block.id.clone(), res); + } + TestStatus::Failed => { + summary.failed += 1; + self.ui.print(TestResult::failed(&display_name)); + failed_names.push(display_name); + } + }, + Err(e) => { summary.failed += 1; self.ui.print(TestResult::failed(&display_name)); failed_names.push(display_name); + self.ui.error(format!("Error running example: {:#}", e)); } - }, - Err(e) => { - summary.failed += 1; - self.ui.print(TestResult::failed(&display_name)); - failed_names.push(display_name); - self.ui.error(format!("Error running example: {:#}", e)); } }, } @@ -161,8 +165,8 @@ impl<'a> TestRunner<'a> { Ok((summary, results)) } - fn run_single(&self, code_block: &CodeBlock, strategy: RunStrategy) -> Result { - let ws = TestWorkspace::new(self.package_metadata, code_block.id.block_index, code_block)?; + fn run_single(&self, code_block: &CodeBlock, strategy: RunStrategy, index: usize) -> Result { + let ws = TestWorkspace::new(self.package_metadata, index , code_block)?; let (actual, print_output, program_output) = self.run_single_inner(&ws, strategy)?; let expected = code_block.expected_outcome(); let status = if actual == expected { diff --git a/extensions/scarb-doc/tests/runnable_examples.rs b/extensions/scarb-doc/tests/runnable_examples.rs index fc2ffe8d8..712e7e340 100644 --- a/extensions/scarb-doc/tests/runnable_examples.rs +++ b/extensions/scarb-doc/tests/runnable_examples.rs @@ -32,12 +32,12 @@ fn supports_runnable_examples() { [..] Running 3 doc examples for `hello_world` test hello_world::bar ... ignored test hello_world::foo ... ignored - [..] Compiling hello_world_example_0 v0.1.0 ([..]) + [..] Compiling hello_world_example_1 v0.1.0 ([..]) [..] Finished `dev` profile target(s) in [..] - [..] Executing hello_world_example_0 + [..] Executing hello_world_example_1 foo bar - Saving output to: target/execute/hello_world_example_0/execution1 + Saving output to: target/execute/hello_world_example_1/execution1 test hello_world::foo_bar ... ok test result: ok. 1 passed; 0 failed; 2 ignored @@ -74,16 +74,16 @@ fn supports_runnable_examples_multiple_per_item() { .success() .stdout_eq(formatdoc! {r#" [..] Running 2 doc examples for `hello_world` - [..] Compiling hello_world_example_0 v0.1.0 ([..]) - [..] Finished `dev` profile target(s) in [..] - [..] Executing hello_world_example_0 - 5 - Saving output to: target/execute/hello_world_example_0/execution1 - test hello_world::add (example 0) ... ok [..] Compiling hello_world_example_1 v0.1.0 ([..]) [..] Finished `dev` profile target(s) in [..] [..] Executing hello_world_example_1 + 5 Saving output to: target/execute/hello_world_example_1/execution1 + test hello_world::add (example 0) ... ok + [..] Compiling hello_world_example_2 v0.1.0 ([..]) + [..] Finished `dev` profile target(s) in [..] + [..] Executing hello_world_example_2 + Saving output to: target/execute/hello_world_example_2/execution1 test hello_world::add (example 1) ... ok test result: ok. 2 passed; 0 failed; 0 ignored @@ -120,13 +120,13 @@ fn runnable_example_fails_at_compile_time() { .failure() .stdout_eq(indoc! {r#" [..] Running 1 doc examples for `hello_world` - [..] Compiling hello_world_example_0 v0.1.0 ([..]) + [..] Compiling hello_world_example_1 v0.1.0 ([..]) error[E0006]: Function not found. --> [..]lib.cairo[..] undefined(); ^^^^^^^^^ - error: could not compile `hello_world_example_0` due to 1 previous error + error: could not compile `hello_world_example_1` due to 1 previous error test hello_world::foo ... FAILED failures: @@ -154,10 +154,10 @@ fn runnable_example_fails_at_runtime() { .failure() .stdout_eq(indoc! {r#" [..] Running 1 doc examples for `hello_world` - [..] Compiling hello_world_example_0 v0.1.0 ([..]) + [..] Compiling hello_world_example_1 v0.1.0 ([..]) [..] Finished `dev` profile target(s) in [..] - [..] Executing hello_world_example_0 - error: Panicked with [..] + [..] Executing hello_world_example_1 + error: Panicked with "Runtime error occurred". test hello_world::foo ... FAILED failures: From 4d893862c10dba6c5e2748e575d59889b3cc6a69 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Wed, 3 Dec 2025 19:36:17 +0300 Subject: [PATCH 26/32] remove unused attrs --- extensions/scarb-doc/src/doc_test/code_blocks.rs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/extensions/scarb-doc/src/doc_test/code_blocks.rs b/extensions/scarb-doc/src/doc_test/code_blocks.rs index ab9eb0a4a..adacb0341 100644 --- a/extensions/scarb-doc/src/doc_test/code_blocks.rs +++ b/extensions/scarb-doc/src/doc_test/code_blocks.rs @@ -39,8 +39,6 @@ pub enum CodeBlockAttribute { Runnable, Ignore, NoRun, - CompileFail, - ShouldPanic, Other(String), } @@ -50,9 +48,7 @@ impl From<&str> for CodeBlockAttribute { "cairo" => CodeBlockAttribute::Cairo, "runnable" => CodeBlockAttribute::Runnable, "ignore" => CodeBlockAttribute::Ignore, - "no_run" | "no-run" => CodeBlockAttribute::NoRun, - "should_panic" | "should-panic" => CodeBlockAttribute::ShouldPanic, - "compile_fail" | "compile-fail" => CodeBlockAttribute::CompileFail, + "no_run" => CodeBlockAttribute::NoRun, _ => CodeBlockAttribute::Other(string.to_string()), } } @@ -105,12 +101,6 @@ impl CodeBlock { if self.attributes.contains(&CodeBlockAttribute::Ignore) { return ExecutionOutcome::None; } - if self.attributes.contains(&CodeBlockAttribute::CompileFail) { - return ExecutionOutcome::CompileError; - } - if self.attributes.contains(&CodeBlockAttribute::ShouldPanic) { - return ExecutionOutcome::RuntimeError; - } if self.attributes.contains(&CodeBlockAttribute::NoRun) { return ExecutionOutcome::BuildSuccess; } From b2a3b8c32def195f452cef57d3a9a65a4b09cf5e Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Thu, 4 Dec 2025 14:54:01 +0300 Subject: [PATCH 27/32] dedup attrs when parsing --- extensions/scarb-doc/src/doc_test/code_blocks.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/extensions/scarb-doc/src/doc_test/code_blocks.rs b/extensions/scarb-doc/src/doc_test/code_blocks.rs index adacb0341..28155d82f 100644 --- a/extensions/scarb-doc/src/doc_test/code_blocks.rs +++ b/extensions/scarb-doc/src/doc_test/code_blocks.rs @@ -3,6 +3,7 @@ use crate::docs_generation::markdown::traits::WithItemDataCommon; use crate::types::crate_type::Crate; use crate::types::module_type::Module; use cairo_lang_doc::parser::DocumentationCommentToken; +use itertools::Itertools; use std::collections::HashMap; use std::str::from_utf8; @@ -112,6 +113,7 @@ impl CodeBlock { .split(',') .map(|attr| attr.trim()) .filter(|attr| !attr.is_empty()) + .dedup() .map(Into::into) .collect() } From 381da73739097effe20600235c393453c51062fe Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Thu, 4 Dec 2025 14:54:06 +0300 Subject: [PATCH 28/32] format --- extensions/scarb-doc/src/doc_test/runner.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/extensions/scarb-doc/src/doc_test/runner.rs b/extensions/scarb-doc/src/doc_test/runner.rs index cf610f435..f6201f8f7 100644 --- a/extensions/scarb-doc/src/doc_test/runner.rs +++ b/extensions/scarb-doc/src/doc_test/runner.rs @@ -116,7 +116,7 @@ impl<'a> TestRunner<'a> { &format!("{} doc examples for `{pkg_name}`", code_blocks.len()), )); - let mut idx = 0; + let mut idx = 0; for block in code_blocks { let strategy = block.run_strategy(); let total_in_item = *blocks_per_item.get(&block.id.item_full_path).unwrap_or(&1); @@ -127,7 +127,7 @@ impl<'a> TestRunner<'a> { summary.ignored += 1; self.ui.print(TestResult::ignored(&display_name)); } - _ => { + _ => { idx += 1; match self.run_single(block, strategy, idx) { Ok(res) => match res.status { @@ -149,7 +149,7 @@ impl<'a> TestRunner<'a> { self.ui.error(format!("Error running example: {:#}", e)); } } - }, + } } } // TODO: add struct with `impl Message` to display this @@ -165,8 +165,13 @@ impl<'a> TestRunner<'a> { Ok((summary, results)) } - fn run_single(&self, code_block: &CodeBlock, strategy: RunStrategy, index: usize) -> Result { - let ws = TestWorkspace::new(self.package_metadata, index , code_block)?; + fn run_single( + &self, + code_block: &CodeBlock, + strategy: RunStrategy, + index: usize, + ) -> Result { + let ws = TestWorkspace::new(self.package_metadata, index, code_block)?; let (actual, print_output, program_output) = self.run_single_inner(&ws, strategy)?; let expected = code_block.expected_outcome(); let status = if actual == expected { From 1975f89d322c1e39b03e98c0e93328d0c67fe878 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Thu, 4 Dec 2025 16:26:26 +0300 Subject: [PATCH 29/32] fix test & refactor; use `AdditionalMetadata` instead of `PackageMetadata` --- extensions/scarb-doc/src/doc_test/runner.rs | 18 ++++++++---------- extensions/scarb-doc/src/doc_test/workspace.rs | 10 +++++++--- .../scarb-doc/src/docs_generation/markdown.rs | 3 ++- extensions/scarb-doc/src/lib.rs | 8 ++++---- extensions/scarb-doc/src/main.rs | 2 +- 5 files changed, 22 insertions(+), 19 deletions(-) diff --git a/extensions/scarb-doc/src/doc_test/runner.rs b/extensions/scarb-doc/src/doc_test/runner.rs index f6201f8f7..973e3ff7b 100644 --- a/extensions/scarb-doc/src/doc_test/runner.rs +++ b/extensions/scarb-doc/src/doc_test/runner.rs @@ -1,3 +1,4 @@ +use crate::AdditionalMetadata; use crate::doc_test::code_blocks::{CodeBlock, CodeBlockId, count_blocks_per_item}; use crate::doc_test::ui::TestResult; use crate::doc_test::workspace::TestWorkspace; @@ -7,7 +8,7 @@ use scarb_execute_utils::{ EXECUTE_PRINT_OUTPUT_FILENAME, EXECUTE_PROGRAM_OUTPUT_FILENAME, incremental_create_execution_output_dir, }; -use scarb_metadata::{PackageMetadata, ScarbCommand}; +use scarb_metadata::ScarbCommand; use scarb_ui::Ui; use scarb_ui::components::{NewLine, Status}; use serde::Serialize; @@ -76,7 +77,7 @@ pub enum ExecutionOutcome { None, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum RunStrategy { Ignore, Build, @@ -91,20 +92,17 @@ pub enum RunStrategy { /// Note: it is expected examples (`code_blocks`) that this runner executes only depend on the target package and standard libraries. pub struct TestRunner<'a> { /// Metadata of the target package whose documentation is being tested. - package_metadata: &'a PackageMetadata, + metadata: &'a AdditionalMetadata, ui: Ui, } impl<'a> TestRunner<'a> { - pub fn new(package_metadata: &'a PackageMetadata, ui: Ui) -> Self { - Self { - package_metadata, - ui, - } + pub fn new(metadata: &'a AdditionalMetadata, ui: Ui) -> Self { + Self { metadata, ui } } pub fn run_all(&self, code_blocks: &[CodeBlock]) -> Result<(TestSummary, ExecutionResults)> { - let pkg_name = &self.package_metadata.name; + let pkg_name = &self.metadata.name; let mut results = HashMap::new(); let mut summary = TestSummary::default(); @@ -171,7 +169,7 @@ impl<'a> TestRunner<'a> { strategy: RunStrategy, index: usize, ) -> Result { - let ws = TestWorkspace::new(self.package_metadata, index, code_block)?; + let ws = TestWorkspace::new(&self.metadata, index, code_block)?; let (actual, print_output, program_output) = self.run_single_inner(&ws, strategy)?; let expected = code_block.expected_outcome(); let status = if actual == expected { diff --git a/extensions/scarb-doc/src/doc_test/workspace.rs b/extensions/scarb-doc/src/doc_test/workspace.rs index 22a9e3f15..194e8fa7f 100644 --- a/extensions/scarb-doc/src/doc_test/workspace.rs +++ b/extensions/scarb-doc/src/doc_test/workspace.rs @@ -1,10 +1,10 @@ +use crate::AdditionalMetadata; use crate::doc_test::code_blocks::CodeBlock; use anyhow::{Context, Result, anyhow}; use cairo_lang_filesystem::db::Edition; use camino::{Utf8Path, Utf8PathBuf}; use indoc::formatdoc; use scarb_build_metadata::CAIRO_VERSION; -use scarb_metadata::PackageMetadata; use std::fmt::Write; use std::fs; use tempfile::{TempDir, tempdir}; @@ -16,7 +16,11 @@ pub(crate) struct TestWorkspace { } impl TestWorkspace { - pub fn new(metadata: &PackageMetadata, index: usize, code_block: &CodeBlock) -> Result { + pub fn new( + metadata: &AdditionalMetadata, + index: usize, + code_block: &CodeBlock, + ) -> Result { let temp_dir = tempdir().context("failed to create temporary workspace")?; let root = Utf8PathBuf::from_path_buf(temp_dir.path().to_path_buf()) .map_err(|path| anyhow!("path `{}` is not UTF-8 encoded", path.display()))?; @@ -46,7 +50,7 @@ impl TestWorkspace { &self.package_name } - fn write_manifest(&self, metadata: &PackageMetadata) -> Result<()> { + fn write_manifest(&self, metadata: &AdditionalMetadata) -> Result<()> { let package_dir = metadata .manifest_path .parent() diff --git a/extensions/scarb-doc/src/docs_generation/markdown.rs b/extensions/scarb-doc/src/docs_generation/markdown.rs index 5d0211912..3a0794e55 100644 --- a/extensions/scarb-doc/src/docs_generation/markdown.rs +++ b/extensions/scarb-doc/src/docs_generation/markdown.rs @@ -3,7 +3,7 @@ use crate::docs_generation::markdown::book_toml::generate_book_toml_content; use crate::docs_generation::markdown::summary::generate_summary_file_content; use crate::errors::{IODirectoryCreationError, IOWriteError}; use anyhow::Result; -use camino::Utf8Path; +use camino::{Utf8Path, Utf8PathBuf}; use std::collections::HashMap; use std::fs; @@ -104,6 +104,7 @@ fn package_information_placeholder() -> crate::AdditionalMetadata { crate::AdditionalMetadata { name: "workspace".to_string(), authors: None, + manifest_path: Utf8PathBuf::from("Scarb.toml"), } } diff --git a/extensions/scarb-doc/src/lib.rs b/extensions/scarb-doc/src/lib.rs index a37b70671..2c14569d9 100644 --- a/extensions/scarb-doc/src/lib.rs +++ b/extensions/scarb-doc/src/lib.rs @@ -17,6 +17,7 @@ use cairo_lang_filesystem::{ ids::{CrateId, CrateLongId}, }; use cairo_lang_utils::Intern; +use camino::Utf8PathBuf; use errors::DiagnosticError; use itertools::Itertools; use scarb_metadata::{ @@ -40,20 +41,20 @@ pub mod versioned_json_output; pub struct PackageInformation<'db> { pub crate_: Crate<'db>, pub metadata: AdditionalMetadata, - pub package_metadata: PackageMetadata, } #[derive(Serialize, Clone)] pub struct AdditionalMetadata { pub name: String, pub authors: Option>, + #[serde(skip)] + pub manifest_path: Utf8PathBuf, } pub struct PackageContext { pub db: ScarbDocDatabase, pub should_document_private_items: bool, pub metadata: AdditionalMetadata, - pub package_metadata: PackageMetadata, package_compilation_unit: Option, main_component: CompilationUnitComponentMetadata, } @@ -101,10 +102,10 @@ pub fn generate_package_context( should_document_private_items, package_compilation_unit, main_component: main_component.clone(), - package_metadata: package_metadata.clone(), metadata: AdditionalMetadata { name: package_metadata.name.clone(), authors, + manifest_path: package_metadata.manifest_path.clone(), }, }) } @@ -145,7 +146,6 @@ pub fn generate_package_information( Ok(PackageInformation { crate_, metadata: context.metadata.clone(), - package_metadata: context.package_metadata.clone(), }) } diff --git a/extensions/scarb-doc/src/main.rs b/extensions/scarb-doc/src/main.rs index 08957a3b2..6970ebf4a 100644 --- a/extensions/scarb-doc/src/main.rs +++ b/extensions/scarb-doc/src/main.rs @@ -85,7 +85,7 @@ fn run_doc_tests(package: &PackageInformation, ui: &Ui) -> Result Date: Thu, 4 Dec 2025 15:30:18 +0300 Subject: [PATCH 30/32] misc cleanup --- .../scarb-doc/src/doc_test/code_blocks.rs | 2 +- extensions/scarb-doc/src/doc_test/ui.rs | 23 ++----------------- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/extensions/scarb-doc/src/doc_test/code_blocks.rs b/extensions/scarb-doc/src/doc_test/code_blocks.rs index 28155d82f..82229882b 100644 --- a/extensions/scarb-doc/src/doc_test/code_blocks.rs +++ b/extensions/scarb-doc/src/doc_test/code_blocks.rs @@ -34,7 +34,7 @@ impl CodeBlockId { } } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum CodeBlockAttribute { Cairo, Runnable, diff --git a/extensions/scarb-doc/src/doc_test/ui.rs b/extensions/scarb-doc/src/doc_test/ui.rs index f5c5ab33c..e3e3f44cd 100644 --- a/extensions/scarb-doc/src/doc_test/ui.rs +++ b/extensions/scarb-doc/src/doc_test/ui.rs @@ -33,23 +33,16 @@ impl Message for TestSummary { #[derive(Serialize)] pub struct TestResult<'a> { name: &'a str, - #[serde(skip)] status: TestResultStatus, } -#[derive(Clone, Copy)] +#[derive(Serialize)] pub enum TestResultStatus { Ok, Failed, Ignored, } -impl Serialize for TestResultStatus { - fn serialize(&self, ser: S) -> Result { - ser.serialize_str(self.as_str()) - } -} - impl TestResultStatus { fn as_str(&self) -> &'static str { match self { @@ -96,18 +89,6 @@ impl Message for TestResult<'_> { } fn structured(self, ser: S) -> Result { - TestResultJson { - r#type: "test_result", - name: self.name, - status: self.status, - } - .serialize(ser) + self.serialize(ser) } } - -#[derive(Serialize)] -struct TestResultJson<'a> { - r#type: &'static str, - name: &'a str, - status: TestResultStatus, -} From 8493e852968b7fcce745d26f4a13884aeb097b7c Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Thu, 4 Dec 2025 17:08:08 +0300 Subject: [PATCH 31/32] format --- extensions/scarb-doc/src/doc_test/runner.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/scarb-doc/src/doc_test/runner.rs b/extensions/scarb-doc/src/doc_test/runner.rs index 973e3ff7b..aaef61fe9 100644 --- a/extensions/scarb-doc/src/doc_test/runner.rs +++ b/extensions/scarb-doc/src/doc_test/runner.rs @@ -169,7 +169,7 @@ impl<'a> TestRunner<'a> { strategy: RunStrategy, index: usize, ) -> Result { - let ws = TestWorkspace::new(&self.metadata, index, code_block)?; + let ws = TestWorkspace::new(self.metadata, index, code_block)?; let (actual, print_output, program_output) = self.run_single_inner(&ws, strategy)?; let expected = code_block.expected_outcome(); let status = if actual == expected { From 22357ecabd43c06595f439b3e75e3032edf1dfc7 Mon Sep 17 00:00:00 2001 From: Maksim Zdobnikau Date: Mon, 22 Dec 2025 18:46:07 +0300 Subject: [PATCH 32/32] post-rebase fix --- Cargo.lock | 2 +- extensions/scarb-doc/Cargo.toml | 2 +- extensions/scarb-doc/src/doc_test/runner.rs | 7 +++---- extensions/scarb-doc/src/types/other_types.rs | 7 +++++-- extensions/scarb-doc/src/types/struct_types.rs | 9 +++++++-- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 602fde9bb..a42a6329f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6934,8 +6934,8 @@ dependencies = [ "mimalloc", "salsa", "scarb-build-metadata", - "scarb-execute-utils", "scarb-extensions-cli", + "scarb-fs-utils", "scarb-metadata 1.15.1", "scarb-test-support", "scarb-ui", diff --git a/extensions/scarb-doc/Cargo.toml b/extensions/scarb-doc/Cargo.toml index f1e91445f..f306d88c0 100644 --- a/extensions/scarb-doc/Cargo.toml +++ b/extensions/scarb-doc/Cargo.toml @@ -32,7 +32,7 @@ scarb-metadata = { path = "../../scarb-metadata" } scarb-build-metadata = { path = "../../utils/scarb-build-metadata" } scarb-ui = { path = "../../utils/scarb-ui" } scarb-extensions-cli = { path = "../../utils/scarb-extensions-cli", default-features = false, features = ["doc"] } -scarb-execute-utils = { path = "../../utils/scarb-execute-utils" } +scarb-fs-utils = { path = "../../utils/scarb-fs-utils" } serde.workspace = true serde_json.workspace = true salsa.workspace = true diff --git a/extensions/scarb-doc/src/doc_test/runner.rs b/extensions/scarb-doc/src/doc_test/runner.rs index aaef61fe9..9e386d240 100644 --- a/extensions/scarb-doc/src/doc_test/runner.rs +++ b/extensions/scarb-doc/src/doc_test/runner.rs @@ -4,9 +4,8 @@ use crate::doc_test::ui::TestResult; use crate::doc_test::workspace::TestWorkspace; use anyhow::Result; use create_output_dir::create_output_dir; -use scarb_execute_utils::{ - EXECUTE_PRINT_OUTPUT_FILENAME, EXECUTE_PROGRAM_OUTPUT_FILENAME, - incremental_create_execution_output_dir, +use scarb_fs_utils::{ + EXECUTE_PRINT_OUTPUT_FILENAME, EXECUTE_PROGRAM_OUTPUT_FILENAME, incremental_create_dir_unique, }; use scarb_metadata::ScarbCommand; use scarb_ui::Ui; @@ -212,7 +211,7 @@ impl<'a> TestRunner<'a> { let output_dir = target_dir.join("execute").join(ws.package_name()); create_output_dir(output_dir.as_std_path())?; - let (output_dir, execution_id) = incremental_create_execution_output_dir(&output_dir)?; + let (output_dir, execution_id) = incremental_create_dir_unique(&output_dir, "execution")?; let run_result = ScarbCommand::new() .arg("execute") diff --git a/extensions/scarb-doc/src/types/other_types.rs b/extensions/scarb-doc/src/types/other_types.rs index 49fd89357..de6678e91 100644 --- a/extensions/scarb-doc/src/types/other_types.rs +++ b/extensions/scarb-doc/src/types/other_types.rs @@ -2,18 +2,21 @@ use crate::db::ScarbDocDatabase; use crate::docs_generation::markdown::context::IncludedItems; use crate::docs_generation::markdown::traits::WithItemDataCommon; use crate::types::item_data::{ItemData, SubItemData}; +use crate::types::module_type::is_doc_hidden_attr; use cairo_lang_defs::ids::NamedLanguageElementId; use cairo_lang_defs::ids::{ ConstantId, EnumId, ExternFunctionId, ExternTypeId, FreeFunctionId, ImplAliasId, ImplConstantDefId, ImplDefId, ImplFunctionId, ImplItemId, ImplTypeDefId, LanguageElementId, - LookupItemId, MacroDeclarationId, ModuleId, ModuleItemId, ModuleTypeAliasId, TraitConstantId, - TraitFunctionId, TraitId, TraitItemId, TraitTypeId, VariantId, + LookupItemId, MacroDeclarationId, MemberId, ModuleId, ModuleItemId, ModuleTypeAliasId, + StructId, TraitConstantId, TraitFunctionId, TraitId, TraitItemId, TraitTypeId, VariantId, }; use cairo_lang_diagnostics::Maybe; use cairo_lang_doc::documentable_item::DocumentableItemId; use cairo_lang_semantic::items::enm::EnumSemantic; use cairo_lang_semantic::items::imp::ImplSemantic; +use cairo_lang_semantic::items::structure::StructSemantic; use cairo_lang_semantic::items::trt::TraitSemantic; +use cairo_lang_semantic::items::visibility::Visibility; use cairo_lang_syntax::node::ast; use serde::Serialize; use std::collections::HashMap; diff --git a/extensions/scarb-doc/src/types/struct_types.rs b/extensions/scarb-doc/src/types/struct_types.rs index d9465c525..72c5b3540 100644 --- a/extensions/scarb-doc/src/types/struct_types.rs +++ b/extensions/scarb-doc/src/types/struct_types.rs @@ -1,6 +1,6 @@ use crate::db::ScarbDocDatabase; use crate::docs_generation::markdown::context::IncludedItems; -use crate::docs_generation::markdown::traits::WithPath; +use crate::docs_generation::markdown::traits::WithItemDataCommon; use crate::location_links::DocLocationLink; use crate::types::item_data::{ItemData, SubItemData}; use crate::types::module_type::is_doc_hidden_attr; @@ -61,7 +61,12 @@ impl<'db> Struct<'db> { pub fn get_all_item_ids<'a>(&'a self) -> IncludedItems<'a, 'db> { self.members .iter() - .map(|item| (item.item_data.id, &item.item_data as &dyn WithPath)) + .map(|item| { + ( + item.item_data.id, + &item.item_data as &dyn WithItemDataCommon, + ) + }) .collect() } }