From 5c83bd84bf8ebdfcb702666fba1cbf067bc4a2df Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Tue, 2 Aug 2022 18:09:11 +0300 Subject: [PATCH 01/67] init --- Cargo.lock | 31 +- Cargo.toml | 1 + doc/Cargo.toml | 27 ++ doc/README.md | 1 + doc/src/config.rs | 7 + doc/src/context.rs | 8 + doc/src/lib.rs | 108 +++++++ doc/src/stub.rs | 165 +++++++++++ doc/testdata/Governor.sol | 596 ++++++++++++++++++++++++++++++++++++++ 9 files changed, 941 insertions(+), 3 deletions(-) create mode 100644 doc/Cargo.toml create mode 100644 doc/README.md create mode 100644 doc/src/config.rs create mode 100644 doc/src/context.rs create mode 100644 doc/src/lib.rs create mode 100644 doc/src/stub.rs create mode 100644 doc/testdata/Governor.sol diff --git a/Cargo.lock b/Cargo.lock index 188f12c85947..8d7b331d9519 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1302,6 +1302,19 @@ dependencies = [ "winapi", ] +[[package]] +name = "doc" +version = "0.1.0" +dependencies = [ + "clap", + "ethers-solc", + "eyre", + "forge-fmt", + "foundry-common", + "solang-parser 0.1.16 (git+https://github.com/hyperledger-labs/solang)", + "thiserror", +] + [[package]] name = "dunce" version = "1.0.2" @@ -1692,7 +1705,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.2", - "solang-parser", + "solang-parser 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", "svm-rs", "svm-rs-builds", "tempfile", @@ -1912,7 +1925,7 @@ dependencies = [ "itertools", "pretty_assertions", "semver", - "solang-parser", + "solang-parser 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", "thiserror", "toml", ] @@ -1983,7 +1996,7 @@ dependencies = [ "serde_json", "serial_test", "similar", - "solang-parser", + "solang-parser 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", "strsim", "strum 0.24.0", "thiserror", @@ -4671,6 +4684,18 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "solang-parser" +version = "0.1.16" +source = "git+https://github.com/hyperledger-labs/solang#225b367208d0f64458a2eac615717c597aa8ce6e" +dependencies = [ + "itertools", + "lalrpop", + "lalrpop-util", + "phf 0.10.1", + "unicode-xid", +] + [[package]] name = "spin" version = "0.5.2" diff --git a/Cargo.toml b/Cargo.toml index 6b6fab33cb86..15acfb38c3fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "cli/test-utils", "common", "config", + "doc", "evm", "fmt", "forge", diff --git a/doc/Cargo.toml b/doc/Cargo.toml new file mode 100644 index 000000000000..554d94f699db --- /dev/null +++ b/doc/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "doc" +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" +readme = "README.md" + +[dependencies] +# foundry internal +foundry-common = { path = "../common" } +forge-fmt = { path = "../fmt" } + +# ethers +ethers-solc = { git = "https://github.com/gakonst/ethers-rs", default-features = false, features = ["async"] } + +# cli +clap = { version = "3.0.10", features = [ + "derive", + "env", + "unicode", + "wrap_help", +] } + +# misc +solang-parser = { git = "https://github.com/hyperledger-labs/solang" } +eyre = "0.6" +thiserror = "1.0.30" diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 000000000000..805dce50d301 --- /dev/null +++ b/doc/README.md @@ -0,0 +1 @@ +# Documentation (`doc`) diff --git a/doc/src/config.rs b/doc/src/config.rs new file mode 100644 index 000000000000..adf01dcb0666 --- /dev/null +++ b/doc/src/config.rs @@ -0,0 +1,7 @@ +use std::path::PathBuf; + +#[derive(Debug)] +pub struct DocConfig { + pub templates: Option, + pub output: Option, +} diff --git a/doc/src/context.rs b/doc/src/context.rs new file mode 100644 index 000000000000..e1d90abb8e18 --- /dev/null +++ b/doc/src/context.rs @@ -0,0 +1,8 @@ +use ethers_solc::ProjectPathsConfig; + +use crate::config::DocConfig; + +pub trait Context { + fn config(&self) -> eyre::Result; + fn project_paths(&self) -> eyre::Result; +} diff --git a/doc/src/lib.rs b/doc/src/lib.rs new file mode 100644 index 000000000000..64c13da68f9d --- /dev/null +++ b/doc/src/lib.rs @@ -0,0 +1,108 @@ +// use clap::{Parser, ValueHint}; +// use config::DocConfig; +// use context::Context; +// // use ethers_solc::{Graph, Project}; +// use thiserror::Error; + +// use forge_fmt::{Formatter, FormatterConfig, Visitable, Visitor}; +// // use foundry_common::paths::ProjectPathsArgs; +// use solang_parser::pt::{Comment, ContractTy, DocComment, SourceUnit, SourceUnitPart}; + +// mod config; +// mod context; +// mod stub; + +// #[derive(Error, Debug)] +// pub enum DocError {} // TODO: + +// type Result = std::result::Result; + +// #[derive(Debug)] +// pub struct SolidityDoc { +// comments: Vec, +// contracts: Vec, +// } + +// #[derive(Debug)] +// pub struct DocContract { +// name: String, +// type_: ContractTy, // TODO: +// comments: Option, +// functions: Vec, +// } + +// #[derive(Debug)] +// pub struct DocFunction { +// comments: Vec, +// name: String, +// args: Vec, +// returns: Vec, +// } + +// #[derive(Debug)] +// pub struct DocParam { +// name: Option, +// type_: String, // TODO: +// } + +// impl SolidityDoc { +// pub fn new(comments: &Vec) -> Self { +// SolidityDoc { contracts: vec![], comments: comments.clone() } +// } +// } + +// impl Visitor for SolidityDoc { +// type Error = DocError; + +// fn visit_source_unit(&mut self, source_unit: &mut SourceUnit) -> Result<()> { +// for source in source_unit.0.clone() { +// match source { +// SourceUnitPart::ContractDefinition(def) => self.contracts.push(DocContract { +// name: def.name.name, +// type_: def.ty, +// comments: parse_doccomments(self.comments, 0, def.loc.start()), +// functions: vec![], +// }), +// _ => {} +// }; +// } + +// Ok(()) +// } +// } + +// // #[derive(Debug)] +// // pub enum Doc { +// // Build(DocBuildArgs), +// // // Serve, +// // } + +// // #[derive(Debug, Clone, Parser)] +// // pub struct DocBuildArgs { +// // #[clap(flatten, next_help_heading = "PROJECT OPTIONS")] +// // project_paths: ProjectPathsArgs, +// // } + +// // impl DocBuildArgs { +// // fn run(self) { +// // // +// // } +// // } + +// #[cfg(test)] +// mod tests { +// use super::*; +// use std::{fs, path::PathBuf}; + +// #[allow(non_snake_case)] +// #[test] +// fn test_docs() { +// let path = +// PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata").join("Governor.sol"); let +// target = fs::read_to_string(path).unwrap(); let (mut source_pt, source_comments) = +// solang_parser::parse(&target, 1).unwrap(); // let source_comments = +// Comments::new(source_comments, source); let mut d = SolidityDoc::new(&source_comments); +// source_pt.visit(&mut d).unwrap(); +// println!("doc {:?}", d); +// } +// } diff --git a/doc/src/stub.rs b/doc/src/stub.rs new file mode 100644 index 000000000000..529976ea8586 --- /dev/null +++ b/doc/src/stub.rs @@ -0,0 +1,165 @@ +use solang_parser::pt::Comment; + +#[derive(Debug, PartialEq, Clone)] +pub enum DocComment { + Line { comment: DocCommentTag }, + Block { comments: Vec }, +} + +impl DocComment { + pub fn comments(&self) -> Vec<&DocCommentTag> { + match self { + DocComment::Line { comment } => vec![comment], + DocComment::Block { comments } => comments.iter().collect(), + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct DocCommentTag { + pub tag: String, + pub tag_offset: usize, + pub value: String, + pub value_offset: usize, +} + +enum CommentType { + Line, + Block, +} + +/// From the start to end offset, filter all the doc comments out of the comments and parse +/// them into tags with values. +pub fn parse_doccomments(comments: &[Comment], start: usize, end: usize) -> Vec { + // first extract the tags + let mut tags = Vec::new(); + + let lines = filter_comments(comments, start, end); + + for (ty, comment_lines) in lines { + let mut single_tags = Vec::new(); + + for (start_offset, line) in comment_lines { + let mut chars = line.char_indices().peekable(); + + if let Some((_, '@')) = chars.peek() { + // step over @ + let (tag_start, _) = chars.next().unwrap(); + let mut tag_end = tag_start; + + while let Some((offset, c)) = chars.peek() { + if c.is_whitespace() { + break + } + + tag_end = *offset; + + chars.next(); + } + + let leading = + line[tag_end + 1..].chars().take_while(|ch| ch.is_whitespace()).count(); + + // tag value + single_tags.push(DocCommentTag { + tag_offset: start_offset + tag_start + 1, + tag: line[tag_start + 1..tag_end + 1].to_owned(), + value_offset: start_offset + tag_end + leading + 1, + value: line[tag_end + 1..].trim().to_owned(), + }); + } else if !single_tags.is_empty() || !tags.is_empty() { + let line = line.trim(); + if !line.is_empty() { + let single_doc_comment = if let Some(single_tag) = single_tags.last_mut() { + Some(single_tag) + } else if let Some(tag) = tags.last_mut() { + match tag { + DocComment::Line { comment } => Some(comment), + DocComment::Block { comments } => comments.last_mut(), + } + } else { + None + }; + + if let Some(comment) = single_doc_comment { + comment.value.push('\n'); + comment.value.push_str(line); + } + } + } else { + let leading = line.chars().take_while(|ch| ch.is_whitespace()).count(); + + single_tags.push(DocCommentTag { + tag_offset: start_offset + start_offset + leading, + tag: String::from("notice"), + value_offset: start_offset + start_offset + leading, + value: line.trim().to_owned(), + }); + } + } + + match ty { + CommentType::Line if !single_tags.is_empty() => { + tags.push(DocComment::Line { comment: single_tags[0].to_owned() }) + } + CommentType::Block => tags.push(DocComment::Block { comments: single_tags }), + _ => {} + } + } + + tags +} + +/// Convert the comment to lines, stripping whitespace, comment characters and leading * in block +/// comments +fn filter_comments( + comments: &[Comment], + start: usize, + end: usize, +) -> Vec<(CommentType, Vec<(usize, &str)>)> { + let mut res = Vec::new(); + + for comment in comments.iter() { + let mut grouped_comments = Vec::new(); + + match comment { + Comment::DocLine(loc, comment) => { + if loc.start() >= end || loc.end() < start { + continue + } + + // remove the leading /// + let leading = comment[3..].chars().take_while(|ch| ch.is_whitespace()).count(); + + grouped_comments.push((loc.start() + leading + 3, comment[3..].trim())); + + res.push((CommentType::Line, grouped_comments)); + } + Comment::DocBlock(loc, comment) => { + if loc.start() >= end || loc.end() < start { + continue + } + + let mut start = loc.start() + 3; + + let len = comment.len(); + + // remove the leading /** and tailing */ + for s in comment[3..len - 2].lines() { + if let Some((i, _)) = + s.char_indices().find(|(_, ch)| !ch.is_whitespace() && *ch != '*') + { + grouped_comments.push((start + i, s[i..].trim_end())); + } + + start += s.len() + 1; + } + + res.push((CommentType::Block, grouped_comments)); + } + _ => (), + } + } + + res +} diff --git a/doc/testdata/Governor.sol b/doc/testdata/Governor.sol new file mode 100644 index 000000000000..ae34135911c0 --- /dev/null +++ b/doc/testdata/Governor.sol @@ -0,0 +1,596 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (governance/Governor.sol) + +pragma solidity ^0.8.0; + +import "../token/ERC721/IERC721Receiver.sol"; +import "../token/ERC1155/IERC1155Receiver.sol"; +import "../utils/cryptography/ECDSA.sol"; +import "../utils/cryptography/draft-EIP712.sol"; +import "../utils/introspection/ERC165.sol"; +import "../utils/math/SafeCast.sol"; +import "../utils/structs/DoubleEndedQueue.sol"; +import "../utils/Address.sol"; +import "../utils/Context.sol"; +import "../utils/Timers.sol"; +import "./IGovernor.sol"; + +/** + * @dev Core of the governance system, designed to be extended though various modules. + * + * This contract is abstract and requires several function to be implemented in various modules: + * + * - A counting module must implement {quorum}, {_quorumReached}, {_voteSucceeded} and {_countVote} + * - A voting module must implement {_getVotes} + * - Additionanly, the {votingPeriod} must also be implemented + * + * _Available since v4.3._ + */ +abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receiver, IERC1155Receiver { + using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque; + using SafeCast for uint256; + using Timers for Timers.BlockNumber; + + bytes32 public constant BALLOT_TYPEHASH = keccak256("Ballot(uint256 proposalId,uint8 support)"); + bytes32 public constant EXTENDED_BALLOT_TYPEHASH = + keccak256("ExtendedBallot(uint256 proposalId,uint8 support,string reason,bytes params)"); + + struct ProposalCore { + Timers.BlockNumber voteStart; + Timers.BlockNumber voteEnd; + bool executed; + bool canceled; + } + + string private _name; + + mapping(uint256 => ProposalCore) private _proposals; + + // This queue keeps track of the governor operating on itself. Calls to functions protected by the + // {onlyGovernance} modifier needs to be whitelisted in this queue. Whitelisting is set in {_beforeExecute}, + // consumed by the {onlyGovernance} modifier and eventually reset in {_afterExecute}. This ensures that the + // execution of {onlyGovernance} protected calls can only be achieved through successful proposals. + DoubleEndedQueue.Bytes32Deque private _governanceCall; + + /** + * @dev Restricts a function so it can only be executed through governance proposals. For example, governance + * parameter setters in {GovernorSettings} are protected using this modifier. + * + * The governance executing address may be different from the Governor's own address, for example it could be a + * timelock. This can be customized by modules by overriding {_executor}. The executor is only able to invoke these + * functions during the execution of the governor's {execute} function, and not under any other circumstances. Thus, + * for example, additional timelock proposers are not able to change governance parameters without going through the + * governance protocol (since v4.6). + */ + modifier onlyGovernance() { + require(_msgSender() == _executor(), "Governor: onlyGovernance"); + if (_executor() != address(this)) { + bytes32 msgDataHash = keccak256(_msgData()); + // loop until popping the expected operation - throw if deque is empty (operation not authorized) + while (_governanceCall.popFront() != msgDataHash) {} + } + _; + } + + /** + * @dev Sets the value for {name} and {version} + */ + constructor(string memory name_) EIP712(name_, version()) { + _name = name_; + } + + /** + * @dev Function to receive ETH that will be handled by the governor (disabled if executor is a third party contract) + */ + receive() external payable virtual { + require(_executor() == address(this)); + } + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) { + // In addition to the current interfaceId, also support previous version of the interfaceId that did not + // include the castVoteWithReasonAndParams() function as standard + return + interfaceId == + (type(IGovernor).interfaceId ^ + this.castVoteWithReasonAndParams.selector ^ + this.castVoteWithReasonAndParamsBySig.selector ^ + this.getVotesWithParams.selector) || + interfaceId == type(IGovernor).interfaceId || + interfaceId == type(IERC1155Receiver).interfaceId || + super.supportsInterface(interfaceId); + } + + /** + * @dev See {IGovernor-name}. + */ + function name() public view virtual override returns (string memory) { + return _name; + } + + /** + * @dev See {IGovernor-version}. + */ + function version() public view virtual override returns (string memory) { + return "1"; + } + + /** + * @dev See {IGovernor-hashProposal}. + * + * The proposal id is produced by hashing the ABI encoded `targets` array, the `values` array, the `calldatas` array + * and the descriptionHash (bytes32 which itself is the keccak256 hash of the description string). This proposal id + * can be produced from the proposal data which is part of the {ProposalCreated} event. It can even be computed in + * advance, before the proposal is submitted. + * + * Note that the chainId and the governor address are not part of the proposal id computation. Consequently, the + * same proposal (with same operation and same description) will have the same id if submitted on multiple governors + * across multiple networks. This also means that in order to execute the same operation twice (on the same + * governor) the proposer will have to change the description in order to avoid proposal id conflicts. + */ + function hashProposal( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) public pure virtual override returns (uint256) { + return uint256(keccak256(abi.encode(targets, values, calldatas, descriptionHash))); + } + + /** + * @dev See {IGovernor-state}. + */ + function state(uint256 proposalId) public view virtual override returns (ProposalState) { + ProposalCore storage proposal = _proposals[proposalId]; + + if (proposal.executed) { + return ProposalState.Executed; + } + + if (proposal.canceled) { + return ProposalState.Canceled; + } + + uint256 snapshot = proposalSnapshot(proposalId); + + if (snapshot == 0) { + revert("Governor: unknown proposal id"); + } + + if (snapshot >= block.number) { + return ProposalState.Pending; + } + + uint256 deadline = proposalDeadline(proposalId); + + if (deadline >= block.number) { + return ProposalState.Active; + } + + if (_quorumReached(proposalId) && _voteSucceeded(proposalId)) { + return ProposalState.Succeeded; + } else { + return ProposalState.Defeated; + } + } + + /** + * @dev See {IGovernor-proposalSnapshot}. + */ + function proposalSnapshot(uint256 proposalId) public view virtual override returns (uint256) { + return _proposals[proposalId].voteStart.getDeadline(); + } + + /** + * @dev See {IGovernor-proposalDeadline}. + */ + function proposalDeadline(uint256 proposalId) public view virtual override returns (uint256) { + return _proposals[proposalId].voteEnd.getDeadline(); + } + + /** + * @dev Part of the Governor Bravo's interface: _"The number of votes required in order for a voter to become a proposer"_. + */ + function proposalThreshold() public view virtual returns (uint256) { + return 0; + } + + /** + * @dev Amount of votes already cast passes the threshold limit. + */ + function _quorumReached(uint256 proposalId) internal view virtual returns (bool); + + /** + * @dev Is the proposal successful or not. + */ + function _voteSucceeded(uint256 proposalId) internal view virtual returns (bool); + + /** + * @dev Get the voting weight of `account` at a specific `blockNumber`, for a vote as described by `params`. + */ + function _getVotes( + address account, + uint256 blockNumber, + bytes memory params + ) internal view virtual returns (uint256); + + /** + * @dev Register a vote for `proposalId` by `account` with a given `support`, voting `weight` and voting `params`. + * + * Note: Support is generic and can represent various things depending on the voting system used. + */ + function _countVote( + uint256 proposalId, + address account, + uint8 support, + uint256 weight, + bytes memory params + ) internal virtual; + + /** + * @dev Default additional encoded parameters used by castVote methods that don't include them + * + * Note: Should be overridden by specific implementations to use an appropriate value, the + * meaning of the additional params, in the context of that implementation + */ + function _defaultParams() internal view virtual returns (bytes memory) { + return ""; + } + + /** + * @dev See {IGovernor-propose}. + */ + function propose( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + string memory description + ) public virtual override returns (uint256) { + require( + getVotes(_msgSender(), block.number - 1) >= proposalThreshold(), + "Governor: proposer votes below proposal threshold" + ); + + uint256 proposalId = hashProposal(targets, values, calldatas, keccak256(bytes(description))); + + require(targets.length == values.length, "Governor: invalid proposal length"); + require(targets.length == calldatas.length, "Governor: invalid proposal length"); + require(targets.length > 0, "Governor: empty proposal"); + + ProposalCore storage proposal = _proposals[proposalId]; + require(proposal.voteStart.isUnset(), "Governor: proposal already exists"); + + uint64 snapshot = block.number.toUint64() + votingDelay().toUint64(); + uint64 deadline = snapshot + votingPeriod().toUint64(); + + proposal.voteStart.setDeadline(snapshot); + proposal.voteEnd.setDeadline(deadline); + + emit ProposalCreated( + proposalId, + _msgSender(), + targets, + values, + new string[](targets.length), + calldatas, + snapshot, + deadline, + description + ); + + return proposalId; + } + + /** + * @dev See {IGovernor-execute}. + */ + function execute( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) public payable virtual override returns (uint256) { + uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash); + + ProposalState status = state(proposalId); + require( + status == ProposalState.Succeeded || status == ProposalState.Queued, + "Governor: proposal not successful" + ); + _proposals[proposalId].executed = true; + + emit ProposalExecuted(proposalId); + + _beforeExecute(proposalId, targets, values, calldatas, descriptionHash); + _execute(proposalId, targets, values, calldatas, descriptionHash); + _afterExecute(proposalId, targets, values, calldatas, descriptionHash); + + return proposalId; + } + + /** + * @dev Internal execution mechanism. Can be overridden to implement different execution mechanism + */ + function _execute( + uint256, /* proposalId */ + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 /*descriptionHash*/ + ) internal virtual { + string memory errorMessage = "Governor: call reverted without message"; + for (uint256 i = 0; i < targets.length; ++i) { + (bool success, bytes memory returndata) = targets[i].call{value: values[i]}(calldatas[i]); + Address.verifyCallResult(success, returndata, errorMessage); + } + } + + /** + * @dev Hook before execution is triggered. + */ + function _beforeExecute( + uint256, /* proposalId */ + address[] memory targets, + uint256[] memory, /* values */ + bytes[] memory calldatas, + bytes32 /*descriptionHash*/ + ) internal virtual { + if (_executor() != address(this)) { + for (uint256 i = 0; i < targets.length; ++i) { + if (targets[i] == address(this)) { + _governanceCall.pushBack(keccak256(calldatas[i])); + } + } + } + } + + /** + * @dev Hook after execution is triggered. + */ + function _afterExecute( + uint256, /* proposalId */ + address[] memory, /* targets */ + uint256[] memory, /* values */ + bytes[] memory, /* calldatas */ + bytes32 /*descriptionHash*/ + ) internal virtual { + if (_executor() != address(this)) { + if (!_governanceCall.empty()) { + _governanceCall.clear(); + } + } + } + + /** + * @dev Internal cancel mechanism: locks up the proposal timer, preventing it from being re-submitted. Marks it as + * canceled to allow distinguishing it from executed proposals. + * + * Emits a {IGovernor-ProposalCanceled} event. + */ + function _cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) internal virtual returns (uint256) { + uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash); + ProposalState status = state(proposalId); + + require( + status != ProposalState.Canceled && status != ProposalState.Expired && status != ProposalState.Executed, + "Governor: proposal not active" + ); + _proposals[proposalId].canceled = true; + + emit ProposalCanceled(proposalId); + + return proposalId; + } + + /** + * @dev See {IGovernor-getVotes}. + */ + function getVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) { + return _getVotes(account, blockNumber, _defaultParams()); + } + + /** + * @dev See {IGovernor-getVotesWithParams}. + */ + function getVotesWithParams( + address account, + uint256 blockNumber, + bytes memory params + ) public view virtual override returns (uint256) { + return _getVotes(account, blockNumber, params); + } + + /** + * @dev See {IGovernor-castVote}. + */ + function castVote(uint256 proposalId, uint8 support) public virtual override returns (uint256) { + address voter = _msgSender(); + return _castVote(proposalId, voter, support, ""); + } + + /** + * @dev See {IGovernor-castVoteWithReason}. + */ + function castVoteWithReason( + uint256 proposalId, + uint8 support, + string calldata reason + ) public virtual override returns (uint256) { + address voter = _msgSender(); + return _castVote(proposalId, voter, support, reason); + } + + /** + * @dev See {IGovernor-castVoteWithReasonAndParams}. + */ + function castVoteWithReasonAndParams( + uint256 proposalId, + uint8 support, + string calldata reason, + bytes memory params + ) public virtual override returns (uint256) { + address voter = _msgSender(); + return _castVote(proposalId, voter, support, reason, params); + } + + /** + * @dev See {IGovernor-castVoteBySig}. + */ + function castVoteBySig( + uint256 proposalId, + uint8 support, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual override returns (uint256) { + address voter = ECDSA.recover( + _hashTypedDataV4(keccak256(abi.encode(BALLOT_TYPEHASH, proposalId, support))), + v, + r, + s + ); + return _castVote(proposalId, voter, support, ""); + } + + /** + * @dev See {IGovernor-castVoteWithReasonAndParamsBySig}. + */ + function castVoteWithReasonAndParamsBySig( + uint256 proposalId, + uint8 support, + string calldata reason, + bytes memory params, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual override returns (uint256) { + address voter = ECDSA.recover( + _hashTypedDataV4( + keccak256( + abi.encode( + EXTENDED_BALLOT_TYPEHASH, + proposalId, + support, + keccak256(bytes(reason)), + keccak256(params) + ) + ) + ), + v, + r, + s + ); + + return _castVote(proposalId, voter, support, reason, params); + } + + /** + * @dev Internal vote casting mechanism: Check that the vote is pending, that it has not been cast yet, retrieve + * voting weight using {IGovernor-getVotes} and call the {_countVote} internal function. Uses the _defaultParams(). + * + * Emits a {IGovernor-VoteCast} event. + */ + function _castVote( + uint256 proposalId, + address account, + uint8 support, + string memory reason + ) internal virtual returns (uint256) { + return _castVote(proposalId, account, support, reason, _defaultParams()); + } + + /** + * @dev Internal vote casting mechanism: Check that the vote is pending, that it has not been cast yet, retrieve + * voting weight using {IGovernor-getVotes} and call the {_countVote} internal function. + * + * Emits a {IGovernor-VoteCast} event. + */ + function _castVote( + uint256 proposalId, + address account, + uint8 support, + string memory reason, + bytes memory params + ) internal virtual returns (uint256) { + ProposalCore storage proposal = _proposals[proposalId]; + require(state(proposalId) == ProposalState.Active, "Governor: vote not currently active"); + + uint256 weight = _getVotes(account, proposal.voteStart.getDeadline(), params); + _countVote(proposalId, account, support, weight, params); + + if (params.length == 0) { + emit VoteCast(account, proposalId, support, weight, reason); + } else { + emit VoteCastWithParams(account, proposalId, support, weight, reason, params); + } + + return weight; + } + + /** + * @dev Relays a transaction or function call to an arbitrary target. In cases where the governance executor + * is some contract other than the governor itself, like when using a timelock, this function can be invoked + * in a governance proposal to recover tokens or Ether that was sent to the governor contract by mistake. + * Note that if the executor is simply the governor itself, use of `relay` is redundant. + */ + function relay( + address target, + uint256 value, + bytes calldata data + ) external virtual onlyGovernance { + Address.functionCallWithValue(target, data, value); + } + + /** + * @dev Address through which the governor executes action. Will be overloaded by module that execute actions + * through another contract such as a timelock. + */ + function _executor() internal view virtual returns (address) { + return address(this); + } + + /** + * @dev See {IERC721Receiver-onERC721Received}. + */ + function onERC721Received( + address, + address, + uint256, + bytes memory + ) public virtual override returns (bytes4) { + return this.onERC721Received.selector; + } + + /** + * @dev See {IERC1155Receiver-onERC1155Received}. + */ + function onERC1155Received( + address, + address, + uint256, + uint256, + bytes memory + ) public virtual override returns (bytes4) { + return this.onERC1155Received.selector; + } + + /** + * @dev See {IERC1155Receiver-onERC1155BatchReceived}. + */ + function onERC1155BatchReceived( + address, + address, + uint256[] memory, + uint256[] memory, + bytes memory + ) public virtual override returns (bytes4) { + return this.onERC1155BatchReceived.selector; + } +} From b7fb11866c74477a68e7e6ddb4a67fc604fa8209 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Thu, 4 Aug 2022 09:14:43 +0300 Subject: [PATCH 02/67] stuff --- Cargo.lock | 20 +---- doc/Cargo.toml | 2 +- doc/src/lib.rs | 223 +++++++++++++++++++++++++++--------------------- doc/src/stub.rs | 165 ----------------------------------- 4 files changed, 133 insertions(+), 277 deletions(-) delete mode 100644 doc/src/stub.rs diff --git a/Cargo.lock b/Cargo.lock index 82809d8891fc..12d72b1cb8d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1311,7 +1311,7 @@ dependencies = [ "eyre", "forge-fmt", "foundry-common", - "solang-parser 0.1.16 (git+https://github.com/hyperledger-labs/solang)", + "solang-parser", "thiserror", ] @@ -1705,7 +1705,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.2", - "solang-parser 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", + "solang-parser", "svm-rs", "svm-rs-builds", "tempfile", @@ -1925,7 +1925,7 @@ dependencies = [ "itertools", "pretty_assertions", "semver", - "solang-parser 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", + "solang-parser", "thiserror", "toml", ] @@ -1996,7 +1996,7 @@ dependencies = [ "serde_json", "serial_test", "similar", - "solang-parser 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", + "solang-parser", "strsim", "strum 0.24.0", "thiserror", @@ -4703,18 +4703,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "solang-parser" -version = "0.1.16" -source = "git+https://github.com/hyperledger-labs/solang#225b367208d0f64458a2eac615717c597aa8ce6e" -dependencies = [ - "itertools", - "lalrpop", - "lalrpop-util", - "phf 0.10.1", - "unicode-xid", -] - [[package]] name = "spin" version = "0.5.2" diff --git a/doc/Cargo.toml b/doc/Cargo.toml index 554d94f699db..f8c126182562 100644 --- a/doc/Cargo.toml +++ b/doc/Cargo.toml @@ -22,6 +22,6 @@ clap = { version = "3.0.10", features = [ ] } # misc -solang-parser = { git = "https://github.com/hyperledger-labs/solang" } +solang-parser = "0.1.17" eyre = "0.6" thiserror = "1.0.30" diff --git a/doc/src/lib.rs b/doc/src/lib.rs index 64c13da68f9d..24e8a231e6ce 100644 --- a/doc/src/lib.rs +++ b/doc/src/lib.rs @@ -1,43 +1,51 @@ -// use clap::{Parser, ValueHint}; -// use config::DocConfig; -// use context::Context; -// // use ethers_solc::{Graph, Project}; -// use thiserror::Error; - -// use forge_fmt::{Formatter, FormatterConfig, Visitable, Visitor}; -// // use foundry_common::paths::ProjectPathsArgs; -// use solang_parser::pt::{Comment, ContractTy, DocComment, SourceUnit, SourceUnitPart}; - -// mod config; -// mod context; -// mod stub; - -// #[derive(Error, Debug)] -// pub enum DocError {} // TODO: - -// type Result = std::result::Result; - -// #[derive(Debug)] -// pub struct SolidityDoc { -// comments: Vec, -// contracts: Vec, -// } - -// #[derive(Debug)] -// pub struct DocContract { -// name: String, -// type_: ContractTy, // TODO: -// comments: Option, -// functions: Vec, -// } - -// #[derive(Debug)] -// pub struct DocFunction { -// comments: Vec, -// name: String, -// args: Vec, -// returns: Vec, -// } +use clap::{Parser, ValueHint}; +use config::DocConfig; +use context::Context; +// use ethers_solc::{Graph, Project}; +use thiserror::Error; + +use forge_fmt::{Formatter, FormatterConfig, Visitable, Visitor}; +// use foundry_common::paths::ProjectPathsArgs; +use solang_parser::{ + doccomment::{parse_doccomments, DocComment}, + pt::{Comment, ContractTy, FunctionDefinition, SourceUnit, SourceUnitPart}, +}; + +mod config; +mod context; + +#[derive(Error, Debug)] +pub enum DocError {} // TODO: + +type Result = std::result::Result; + +#[derive(Debug)] +pub struct SolidityDoc { + comments: Vec, + contracts: Vec, +} + +#[derive(Debug)] +pub struct DocContract { + ty: ContractTy, + name: String, + comments: Vec, + // TODO: functions: Vec, + parts: Vec, +} + +#[derive(Debug)] +pub enum DocContractPart { + Function(DocFunction), +} + +#[derive(Debug)] +pub struct DocFunction { + comments: Vec, + name: String, + // args: Vec, + // returns: Vec, +} // #[derive(Debug)] // pub struct DocParam { @@ -45,64 +53,89 @@ // type_: String, // TODO: // } -// impl SolidityDoc { -// pub fn new(comments: &Vec) -> Self { -// SolidityDoc { contracts: vec![], comments: comments.clone() } -// } +impl SolidityDoc { + pub fn new(comments: Vec) -> Self { + SolidityDoc { contracts: vec![], comments } + } +} + +impl Visitor for SolidityDoc { + type Error = DocError; + + fn visit_source_unit(&mut self, source_unit: &mut SourceUnit) -> Result<()> { + for source in source_unit.0.iter_mut() { + match source { + SourceUnitPart::ContractDefinition(def) => { + self.contracts.push(DocContract { + name: def.name.name.clone(), + ty: def.ty.clone(), + comments: parse_doccomments(&self.comments, 0, def.loc.start()), + parts: vec![], + }); + for d in def.parts.iter_mut() { + d.visit(self)?; + } + } + SourceUnitPart::FunctionDefinition(def) => {} + _ => {} + }; + } + + Ok(()) + } + + fn visit_function(&mut self, func: &mut FunctionDefinition) -> Result<()> { + Ok(()) + } +} + +// #[derive(Debug)] +// pub enum Doc { +// Build(DocBuildArgs), +// // Serve, // } -// impl Visitor for SolidityDoc { -// type Error = DocError; - -// fn visit_source_unit(&mut self, source_unit: &mut SourceUnit) -> Result<()> { -// for source in source_unit.0.clone() { -// match source { -// SourceUnitPart::ContractDefinition(def) => self.contracts.push(DocContract { -// name: def.name.name, -// type_: def.ty, -// comments: parse_doccomments(self.comments, 0, def.loc.start()), -// functions: vec![], -// }), -// _ => {} -// }; -// } - -// Ok(()) -// } +// #[derive(Debug, Clone, Parser)] +// pub struct DocBuildArgs { +// #[clap(flatten, next_help_heading = "PROJECT OPTIONS")] +// project_paths: ProjectPathsArgs, // } -// // #[derive(Debug)] -// // pub enum Doc { -// // Build(DocBuildArgs), -// // // Serve, -// // } - -// // #[derive(Debug, Clone, Parser)] -// // pub struct DocBuildArgs { -// // #[clap(flatten, next_help_heading = "PROJECT OPTIONS")] -// // project_paths: ProjectPathsArgs, -// // } - -// // impl DocBuildArgs { -// // fn run(self) { -// // // -// // } -// // } - -// #[cfg(test)] -// mod tests { -// use super::*; -// use std::{fs, path::PathBuf}; - -// #[allow(non_snake_case)] -// #[test] -// fn test_docs() { -// let path = -// PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata").join("Governor.sol"); let -// target = fs::read_to_string(path).unwrap(); let (mut source_pt, source_comments) = -// solang_parser::parse(&target, 1).unwrap(); // let source_comments = -// Comments::new(source_comments, source); let mut d = SolidityDoc::new(&source_comments); -// source_pt.visit(&mut d).unwrap(); -// println!("doc {:?}", d); +// impl DocBuildArgs { +// fn run(self) { +// // // } // } + +#[cfg(test)] +mod tests { + use super::*; + use std::{fs, path::PathBuf}; + + #[allow(non_snake_case)] + #[test] + fn test_docs() { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata").join("Governor.sol"); + let target = fs::read_to_string(path).unwrap(); + let (mut source_pt, source_comments) = solang_parser::parse(&target, 1).unwrap(); + // let source_comments = Comments::new(source_comments, source); + let mut d = SolidityDoc::new(source_comments); + source_pt.visit(&mut d).unwrap(); + // println!("doc {:?}", d); + + for contract in d.contracts { + println!("contract >>> {:?}", contract); + // for comment in contract.comments { + // for comment in comment.comments() { + // println!("comment >>> tag: {} val: {}", comment.tag, comment.value); + // } + // } + // let content = match comment { + // Comment::Block(_, comment) | + // Comment::Line(_, comment) | + // Comment::DocLine(_, comment) | + // Comment::DocBlock(_, comment) => comment, + // }; + } + } +} diff --git a/doc/src/stub.rs b/doc/src/stub.rs deleted file mode 100644 index 529976ea8586..000000000000 --- a/doc/src/stub.rs +++ /dev/null @@ -1,165 +0,0 @@ -use solang_parser::pt::Comment; - -#[derive(Debug, PartialEq, Clone)] -pub enum DocComment { - Line { comment: DocCommentTag }, - Block { comments: Vec }, -} - -impl DocComment { - pub fn comments(&self) -> Vec<&DocCommentTag> { - match self { - DocComment::Line { comment } => vec![comment], - DocComment::Block { comments } => comments.iter().collect(), - } - } -} - -#[derive(Debug, PartialEq, Clone)] -pub struct DocCommentTag { - pub tag: String, - pub tag_offset: usize, - pub value: String, - pub value_offset: usize, -} - -enum CommentType { - Line, - Block, -} - -/// From the start to end offset, filter all the doc comments out of the comments and parse -/// them into tags with values. -pub fn parse_doccomments(comments: &[Comment], start: usize, end: usize) -> Vec { - // first extract the tags - let mut tags = Vec::new(); - - let lines = filter_comments(comments, start, end); - - for (ty, comment_lines) in lines { - let mut single_tags = Vec::new(); - - for (start_offset, line) in comment_lines { - let mut chars = line.char_indices().peekable(); - - if let Some((_, '@')) = chars.peek() { - // step over @ - let (tag_start, _) = chars.next().unwrap(); - let mut tag_end = tag_start; - - while let Some((offset, c)) = chars.peek() { - if c.is_whitespace() { - break - } - - tag_end = *offset; - - chars.next(); - } - - let leading = - line[tag_end + 1..].chars().take_while(|ch| ch.is_whitespace()).count(); - - // tag value - single_tags.push(DocCommentTag { - tag_offset: start_offset + tag_start + 1, - tag: line[tag_start + 1..tag_end + 1].to_owned(), - value_offset: start_offset + tag_end + leading + 1, - value: line[tag_end + 1..].trim().to_owned(), - }); - } else if !single_tags.is_empty() || !tags.is_empty() { - let line = line.trim(); - if !line.is_empty() { - let single_doc_comment = if let Some(single_tag) = single_tags.last_mut() { - Some(single_tag) - } else if let Some(tag) = tags.last_mut() { - match tag { - DocComment::Line { comment } => Some(comment), - DocComment::Block { comments } => comments.last_mut(), - } - } else { - None - }; - - if let Some(comment) = single_doc_comment { - comment.value.push('\n'); - comment.value.push_str(line); - } - } - } else { - let leading = line.chars().take_while(|ch| ch.is_whitespace()).count(); - - single_tags.push(DocCommentTag { - tag_offset: start_offset + start_offset + leading, - tag: String::from("notice"), - value_offset: start_offset + start_offset + leading, - value: line.trim().to_owned(), - }); - } - } - - match ty { - CommentType::Line if !single_tags.is_empty() => { - tags.push(DocComment::Line { comment: single_tags[0].to_owned() }) - } - CommentType::Block => tags.push(DocComment::Block { comments: single_tags }), - _ => {} - } - } - - tags -} - -/// Convert the comment to lines, stripping whitespace, comment characters and leading * in block -/// comments -fn filter_comments( - comments: &[Comment], - start: usize, - end: usize, -) -> Vec<(CommentType, Vec<(usize, &str)>)> { - let mut res = Vec::new(); - - for comment in comments.iter() { - let mut grouped_comments = Vec::new(); - - match comment { - Comment::DocLine(loc, comment) => { - if loc.start() >= end || loc.end() < start { - continue - } - - // remove the leading /// - let leading = comment[3..].chars().take_while(|ch| ch.is_whitespace()).count(); - - grouped_comments.push((loc.start() + leading + 3, comment[3..].trim())); - - res.push((CommentType::Line, grouped_comments)); - } - Comment::DocBlock(loc, comment) => { - if loc.start() >= end || loc.end() < start { - continue - } - - let mut start = loc.start() + 3; - - let len = comment.len(); - - // remove the leading /** and tailing */ - for s in comment[3..len - 2].lines() { - if let Some((i, _)) = - s.char_indices().find(|(_, ch)| !ch.is_whitespace() && *ch != '*') - { - grouped_comments.push((start + i, s[i..].trim_end())); - } - - start += s.len() + 1; - } - - res.push((CommentType::Block, grouped_comments)); - } - _ => (), - } - } - - res -} From 304c93d37afd21e1dbb471fd9a74a39e316a2c1c Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Wed, 10 Aug 2022 10:38:17 +0300 Subject: [PATCH 03/67] forge doc cont --- Cargo.lock | 703 ++++++++++++++++-- cli/Cargo.toml | 1 + cli/src/cmd/forge/doc.rs | 163 ++++ cli/src/cmd/forge/mod.rs | 1 + doc/Cargo.toml | 6 +- doc/src/config.rs | 7 - doc/src/context.rs | 8 - doc/src/lib.rs | 179 ++--- doc/testdata/{ => oz-governance}/Governor.sol | 0 9 files changed, 898 insertions(+), 170 deletions(-) create mode 100644 cli/src/cmd/forge/doc.rs delete mode 100644 doc/src/config.rs delete mode 100644 doc/src/context.rs rename doc/testdata/{ => oz-governance}/Governor.sol (100%) diff --git a/Cargo.lock b/Cargo.lock index 12d72b1cb8d1..ccda0d20ff89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,13 +59,26 @@ dependencies = [ "memchr", ] +[[package]] +name = "ammonia" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5ed2509ee88cc023cccee37a6fab35826830fe8b748b3869790e7720c2c4a74" +dependencies = [ + "html5ever", + "maplit", + "once_cell", + "tendril", + "url", +] + [[package]] name = "ansi_term" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -246,7 +259,7 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi", "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -303,10 +316,10 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sha-1", + "sha-1 0.10.0", "sync_wrapper", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.17.2", "tower", "tower-http", "tower-layer", @@ -511,7 +524,9 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" dependencies = [ + "lazy_static", "memchr", + "regex-automata", ] [[package]] @@ -676,7 +691,7 @@ dependencies = [ "num-integer", "num-traits", "time 0.1.43", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -748,7 +763,7 @@ dependencies = [ "terminfo", "thiserror", "which", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -867,7 +882,7 @@ checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" dependencies = [ "atty", "lazy_static", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -913,7 +928,7 @@ dependencies = [ "async-trait", "nix 0.22.3", "tokio", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -928,7 +943,7 @@ dependencies = [ "regex", "terminal_size", "unicode-width", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -943,7 +958,7 @@ dependencies = [ "regex", "terminal_size", "unicode-width", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1056,7 +1071,7 @@ dependencies = [ "parking_lot 0.11.2", "signal-hook", "signal-hook-mio", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1072,7 +1087,7 @@ dependencies = [ "parking_lot 0.11.2", "signal-hook", "signal-hook-mio", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1088,7 +1103,7 @@ dependencies = [ "parking_lot 0.12.0", "signal-hook", "signal-hook-mio", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1097,7 +1112,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a6966607622438301997d3dac0d2f6e9a90c68bb6bc1785ea98456ab93c0507" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1106,7 +1121,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1163,7 +1178,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b37feaa84e6861e00a1f5e5aa8da3ee56d605c9992d33e082786754828e20865" dependencies = [ "nix 0.24.1", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1176,7 +1191,7 @@ dependencies = [ "libc", "schannel", "socket2", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1191,7 +1206,7 @@ dependencies = [ "libz-sys", "pkg-config", "vcpkg", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1288,7 +1303,7 @@ checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" dependencies = [ "libc", "redox_users", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1299,20 +1314,7 @@ checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", "redox_users", - "winapi", -] - -[[package]] -name = "doc" -version = "0.1.0" -dependencies = [ - "clap", - "ethers-solc", - "eyre", - "forge-fmt", - "foundry-common", - "solang-parser", - "thiserror", + "winapi 0.3.9", ] [[package]] @@ -1339,6 +1341,18 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "elasticlunr-rs" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94d9c8df0fe6879ca12e7633fdfe467c503722cc981fc463703472d2b876448" +dependencies = [ + "regex", + "serde", + "serde_derive", + "serde_json", +] + [[package]] name = "elliptic-curve" version = "0.12.2" @@ -1403,6 +1417,19 @@ dependencies = [ "syn", ] +[[package]] +name = "env_logger" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + [[package]] name = "eth-keystore" version = "0.4.2" @@ -1645,7 +1672,7 @@ dependencies = [ "serde_json", "thiserror", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.17.2", "tracing", "tracing-futures", "url", @@ -1838,7 +1865,7 @@ dependencies = [ "cfg-if 1.0.0", "libc", "redox_syscall", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1916,6 +1943,20 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "forge-doc" +version = "0.1.0" +dependencies = [ + "clap", + "ethers-solc", + "eyre", + "forge-fmt", + "foundry-common", + "mdbook", + "solang-parser", + "thiserror", +] + [[package]] name = "forge-fmt" version = "0.2.0" @@ -1972,6 +2013,7 @@ dependencies = [ "ethers", "eyre", "forge", + "forge-doc", "forge-fmt", "foundry-cli-test-utils", "foundry-common", @@ -2133,7 +2175,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" dependencies = [ "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2142,6 +2184,25 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394" +[[package]] +name = "fsevent" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6" +dependencies = [ + "bitflags", + "fsevent-sys 2.0.1", +] + +[[package]] +name = "fsevent-sys" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f41b048a94555da0f42f1d632e2e19510084fb8e303b0daa2816e733fb3644a0" +dependencies = [ + "libc", +] + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -2151,12 +2212,38 @@ dependencies = [ "libc", ] +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +dependencies = [ + "bitflags", + "fuchsia-zircon-sys", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" + [[package]] name = "funty" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + [[package]] name = "futures" version = "0.3.21" @@ -2382,6 +2469,15 @@ dependencies = [ "url", ] +[[package]] +name = "gitignore" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78aa90e4620c1498ac434c06ba6e521b525794bbdacf085d490cc794b4a2f9a4" +dependencies = [ + "glob", +] + [[package]] name = "glob" version = "0.3.0" @@ -2427,10 +2523,24 @@ dependencies = [ "indexmap", "slab", "tokio", - "tokio-util", + "tokio-util 0.7.1", "tracing", ] +[[package]] +name = "handlebars" +version = "4.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "360d9740069b2f6cbb63ce2dbaa71a20d3185350cbb990d7bebeb9318415eb17" +dependencies = [ + "log", + "pest", + "pest_derive", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "hash-db" version = "0.15.2" @@ -2471,6 +2581,31 @@ dependencies = [ "fxhash", ] +[[package]] +name = "headers" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d" +dependencies = [ + "base64 0.13.0", + "bitflags", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha-1 0.10.0", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.3.3" @@ -2531,7 +2666,21 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2456aef2e6b6a9784192ae780c0f15bc57df0e918585282325e8c8ac27737654" dependencies = [ - "winapi", + "winapi 0.3.9", +] + +[[package]] +name = "html5ever" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -2574,6 +2723,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "0.14.18" @@ -2753,6 +2908,17 @@ version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" +[[package]] +name = "inotify" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4816c66d2c8ae673df83366c18341538f234a26d65a9ecea5c348b453ac1d02f" +dependencies = [ + "bitflags", + "inotify-sys", + "libc", +] + [[package]] name = "inotify" version = "0.9.6" @@ -2785,6 +2951,15 @@ dependencies = [ "web-sys", ] +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + [[package]] name = "ipnet" version = "2.5.0" @@ -2843,6 +3018,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7" +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + [[package]] name = "kqueue" version = "1.0.5" @@ -2904,6 +3089,12 @@ dependencies = [ "spin", ] +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.124" @@ -2975,6 +3166,32 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "markup5ever" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" +dependencies = [ + "log", + "phf 0.10.1", + "phf_codegen 0.10.0", + "string_cache", + "string_cache_codegen", + "tendril", +] + [[package]] name = "matchers" version = "0.1.0" @@ -3005,11 +3222,44 @@ dependencies = [ "digest 0.10.3", ] +[[package]] +name = "mdbook" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f3e133c6d515528745ffd3b9f0c7d975ae039f0b6abb099f2168daa2afb4f9" +dependencies = [ + "ammonia", + "anyhow", + "chrono 0.4.19", + "clap", + "clap_complete", + "elasticlunr-rs", + "env_logger", + "futures-util", + "gitignore", + "handlebars", + "lazy_static", + "log", + "memchr", + "notify 4.0.17", + "opener", + "pulldown-cmark", + "regex", + "serde", + "serde_json", + "shlex", + "tempfile", + "tokio", + "toml", + "topological-sort", + "warp", +] + [[package]] name = "memchr" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "memoffset" @@ -3049,6 +3299,16 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -3064,6 +3324,25 @@ dependencies = [ "adler", ] +[[package]] +name = "mio" +version = "0.6.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" +dependencies = [ + "cfg-if 0.1.10", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "iovec", + "kernel32-sys", + "libc", + "log", + "miow 0.2.2", + "net2", + "slab", + "winapi 0.2.8", +] + [[package]] name = "mio" version = "0.7.14" @@ -3072,9 +3351,9 @@ checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" dependencies = [ "libc", "log", - "miow", + "miow 0.3.7", "ntapi", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -3085,10 +3364,34 @@ checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9" dependencies = [ "libc", "log", - "miow", + "miow 0.3.7", "ntapi", "wasi 0.11.0+wasi-snapshot-preview1", - "winapi", + "winapi 0.3.9", +] + +[[package]] +name = "mio-extras" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19" +dependencies = [ + "lazycell", + "log", + "mio 0.6.23", + "slab", +] + +[[package]] +name = "miow" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" +dependencies = [ + "kernel32-sys", + "net2", + "winapi 0.2.8", + "ws2_32-sys", ] [[package]] @@ -3097,7 +3400,7 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -3118,6 +3421,17 @@ dependencies = [ "tempfile", ] +[[package]] +name = "net2" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "winapi 0.3.9", +] + [[package]] name = "new_debug_unreachable" version = "1.0.4" @@ -3181,6 +3495,24 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "notify" +version = "4.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae03c8c853dba7bfd23e571ff0cff7bc9dceb40a4cd684cd1681824183f45257" +dependencies = [ + "bitflags", + "filetime", + "fsevent", + "fsevent-sys 2.0.1", + "inotify 0.7.1", + "libc", + "mio 0.6.23", + "mio-extras", + "walkdir", + "winapi 0.3.9", +] + [[package]] name = "notify" version = "5.0.0-pre.15" @@ -3190,13 +3522,13 @@ dependencies = [ "bitflags", "crossbeam-channel", "filetime", - "fsevent-sys", - "inotify", + "fsevent-sys 4.1.0", + "inotify 0.9.6", "kqueue", "libc", "mio 0.8.2", "walkdir", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -3205,7 +3537,7 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -3368,6 +3700,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "opener" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea3ebcd72a54701f56345f16785a6d3ac2df7e986d273eb4395c0b01db17952" +dependencies = [ + "bstr", + "winapi 0.3.9", +] + [[package]] name = "openssl" version = "0.10.38" @@ -3413,7 +3755,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -3480,7 +3822,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -3577,6 +3919,50 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +[[package]] +name = "pest" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69486e2b8c2d2aeb9762db7b4e00b0331156393555cff467f4163ff06821eef8" +dependencies = [ + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b13570633aff33c6d22ce47dd566b10a3b9122c2fe9d8e7501895905be532b91" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3c567e5702efdc79fb18859ea74c3eb36e14c43da7b8c1f098a4ed6514ec7a0" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb32be5ee3bbdafa8c7a18b0a8a8d962b66cfa2ceee4037f49267a50ee821fe" +dependencies = [ + "once_cell", + "pest", + "sha-1 0.10.0", +] + [[package]] name = "petgraph" version = "0.5.1" @@ -3627,6 +4013,16 @@ dependencies = [ "phf_shared 0.8.0", ] +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", +] + [[package]] name = "phf_generator" version = "0.8.0" @@ -3869,6 +4265,17 @@ version = "2.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf7e6d18738ecd0902d30d1ad232c9125985a3422929b16c65517b38adc14f96" +[[package]] +name = "pulldown-cmark" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d9cc634bc78768157b5cbfe988ffcd1dcba95cd2b2f03a88316c08c6d00ed63" +dependencies = [ + "bitflags", + "memchr", + "unicase", +] + [[package]] name = "pwd" version = "1.3.1" @@ -4078,7 +4485,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -4182,7 +4589,7 @@ dependencies = [ "spin", "untrusted", "web-sys", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -4222,7 +4629,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffc936cf8a7ea60c58f030fd36a612a48f440610214dc54bc36431f9ea0c3efb" dependencies = [ "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -4337,9 +4744,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" dependencies = [ "lazy_static", - "winapi", + "winapi 0.3.9", ] +[[package]] +name = "scoped-tls" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" + [[package]] name = "scopeguard" version = "1.1.0" @@ -4527,6 +4940,19 @@ dependencies = [ "syn", ] +[[package]] +name = "sha-1" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.9.0", + "opaque-debug 0.3.0", +] + [[package]] name = "sha-1" version = "0.10.0" @@ -4615,6 +5041,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + [[package]] name = "signal-hook" version = "0.3.13" @@ -4687,7 +5119,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" dependencies = [ "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -4736,6 +5168,19 @@ dependencies = [ "parking_lot 0.12.0", "phf_shared 0.10.0", "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro2", + "quote", ] [[package]] @@ -4894,7 +5339,18 @@ dependencies = [ "libc", "redox_syscall", "remove_dir_all", - "winapi", + "winapi 0.3.9", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", ] [[package]] @@ -4905,7 +5361,7 @@ checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" dependencies = [ "dirs-next", "rustversion", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -4924,7 +5380,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" dependencies = [ "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -4937,7 +5393,7 @@ dependencies = [ "fnv", "nom 5.1.2", "phf 0.8.0", - "phf_codegen", + "phf_codegen 0.8.0", ] [[package]] @@ -4986,7 +5442,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" dependencies = [ "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -5048,7 +5504,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -5094,6 +5550,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-tungstenite" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "511de3f85caf1c98983545490c3d09685fa8eb634e57eec22bb4db271f46cbd8" +dependencies = [ + "futures-util", + "log", + "pin-project", + "tokio", + "tungstenite 0.14.0", +] + [[package]] name = "tokio-tungstenite" version = "0.17.2" @@ -5107,11 +5576,25 @@ dependencies = [ "tokio", "tokio-native-tls", "tokio-rustls", - "tungstenite", + "tungstenite 0.17.3", "webpki", "webpki-roots", ] +[[package]] +name = "tokio-util" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.1" @@ -5147,6 +5630,12 @@ dependencies = [ "itertools", ] +[[package]] +name = "topological-sort" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa7c7f42dea4b1b99439786f5633aeb9c14c1b53f75e282803c2ec2ad545873c" + [[package]] name = "tower" version = "0.4.12" @@ -5158,7 +5647,7 @@ dependencies = [ "pin-project", "pin-project-lite", "tokio", - "tokio-util", + "tokio-util 0.7.1", "tower-layer", "tower-service", "tracing", @@ -5323,6 +5812,25 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "tungstenite" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0b2d8558abd2e276b0a8df5c05a2ec762609344191e5fd23e292c910e9165b5" +dependencies = [ + "base64 0.13.0", + "byteorder", + "bytes", + "http", + "httparse", + "log", + "rand 0.8.5", + "sha-1 0.9.8", + "thiserror", + "url", + "utf-8", +] + [[package]] name = "tungstenite" version = "0.17.3" @@ -5338,7 +5846,7 @@ dependencies = [ "native-tls", "rand 0.8.5", "rustls", - "sha-1", + "sha-1 0.10.0", "thiserror", "url", "utf-8", @@ -5351,6 +5859,12 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +[[package]] +name = "ucd-trie" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89570599c4fe5585de2b388aab47e99f7fa4e9238a1399f707a02e356058141c" + [[package]] name = "ui" version = "0.2.0" @@ -5530,7 +6044,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" dependencies = [ "same-file", - "winapi", + "winapi 0.3.9", "winapi-util", ] @@ -5544,6 +6058,35 @@ dependencies = [ "try-lock", ] +[[package]] +name = "warp" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cef4e1e9114a4b7f1ac799f16ce71c14de5778500c5450ec6b7b920c55b587e" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "headers", + "http", + "hyper", + "log", + "mime", + "mime_guess", + "percent-encoding", + "pin-project", + "scoped-tls", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-stream", + "tokio-tungstenite 0.15.0", + "tokio-util 0.6.10", + "tower-service", + "tracing", +] + [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" @@ -5659,7 +6202,7 @@ dependencies = [ "ignore-files", "libc", "miette", - "notify", + "notify 5.0.0-pre.15", "once_cell", "project-origins", "thiserror", @@ -5707,6 +6250,12 @@ dependencies = [ "libc", ] +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + [[package]] name = "winapi" version = "0.3.9" @@ -5717,6 +6266,12 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu", ] +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" @@ -5729,7 +6284,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -5787,7 +6342,17 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" dependencies = [ - "winapi", + "winapi 0.3.9", +] + +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +dependencies = [ + "winapi 0.2.8", + "winapi-build", ] [[package]] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 33991aa6fde3..6ce29099c8cb 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -17,6 +17,7 @@ vergen = { version = "7.0", default-features = false, features = [ [dependencies] # foundry internal forge-fmt = { path = "../fmt" } +forge-doc = { path = "../doc" } foundry-utils = { path = "../utils" } forge = { path = "../forge" } foundry-config = { path = "../config" } diff --git a/cli/src/cmd/forge/doc.rs b/cli/src/cmd/forge/doc.rs new file mode 100644 index 000000000000..44bb1f4bdfab --- /dev/null +++ b/cli/src/cmd/forge/doc.rs @@ -0,0 +1,163 @@ +use crate::cmd::Cmd; +use clap::{Parser, ValueHint}; +use forge_doc::{SolidityDoc, SolidityDocPart, SolidityDocPartElement}; +use forge_fmt::Visitable; +use foundry_config::load_config_with_root; +use rayon::prelude::*; +use solang_parser::doccomment::DocComment; +use std::{fs, path::PathBuf}; + +#[derive(Debug, Clone, Parser)] +pub struct DocArgs { + #[clap( + help = "The project's root path.", + long_help = "The project's root path. By default, this is the root directory of the current Git repository, or the current working directory.", + long, + value_hint = ValueHint::DirPath, + value_name = "PATH" + )] + root: Option, +} + +impl DocArgs { + fn format_comments(&self, comments: &Vec) -> String { + comments + .iter() + .map(|c| match c { + DocComment::Line { comment } => { + // format!("Tag: {}. {}", comment.tag, comment.value) + format!("{}", comment.value) + } + DocComment::Block { comments } => comments + .iter() + .map(|comment| + // format!("Tag: {}. {}", comment.tag, comment.value) + format!("{}", comment.value)) + .collect::>() + .join("\n"), + }) + .collect::>() + .join("\n") + } + + pub fn write_docs(&self, parts: &Vec) -> Result<(), eyre::Error> { + // TODO: + let out_dir = + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata").join("oz-governance"); + std::fs::create_dir_all(&out_dir.join("docs"))?; + for part in parts.iter() { + if let SolidityDocPartElement::Contract(ref contract) = part.element { + let mut out = vec![ + format!("# {}", contract.name), + "".to_owned(), + self.format_comments(&part.comments), + "".to_owned(), + ]; + + let mut attributes = vec![]; + let mut funcs = vec![]; + + for child in part.children.iter() { + match &child.element { + SolidityDocPartElement::Function(func) => { + funcs.push((func.clone(), child.comments.clone())) + } + SolidityDocPartElement::Variable(var) => { + attributes.push((var.clone(), child.comments.clone())) + } + _ => {} + } + } + + out.push("## Attributes".to_owned()); + out.extend(attributes.iter().flat_map(|(var, comments)| { + vec![ + format!("### {}", var.name.name), + self.format_comments(&comments), + "".to_owned(), + ] + })); + + out.push("## Functions".to_owned()); + out.extend(funcs.iter().flat_map(|(func, comments)| { + vec![ + format!( + "### {}", + func.name.as_ref().map_or(func.ty.to_string(), |n| n.name.to_owned()) + ), + self.format_comments(&comments), + "".to_owned(), + ] + })); + + std::fs::write(out_dir.join("docs").join("Governor.md"), out.join("\n"))?; + } + + std::fs::write( + out_dir.join("docs").join("SUMMARY.md"), + "# OZ Governance\n - [Governor](Governor.md)", + )?; + std::fs::write( + out_dir.join("book.toml"), + "[book]\ntitle = \"OZ Governance\"\nsrc = \"docs\"", + )?; + } + // let title = "Test"; + // let summary: Summary = Summary { + // title: Some(title.to_owned()), + // prefix_chapters: vec![], + // numbered_chapters: vec![ + // SummaryItem::PartTitle("Contracts".to_owned()), + // SummaryItem::Link(Link::new("Governor2", + // src_dir.join("src").join("Governor.md"))), ], + // suffix_chapters: vec![], + // }; + // let mut config = Config::default(); + // config.book = BookConfig { + // title: Some(title.to_owned()), + // language: Some("solidity".to_owned()), + // authors: vec![], + // description: None, + // multilingual: false, + // src: src_dir.clone(), + // }; + // config.build = BuildConfig { + // build_dir: src_dir.join("book"), + // create_missing: false, + // use_default_preprocessors: true, + // }; + // MDBook::load_with_config_and_summary(env!("CARGO_MANIFEST_DIR"), config, summary) + // .unwrap() + // .build() + // .unwrap(); + Ok(()) + } +} + +impl Cmd for DocArgs { + type Output = (); + + fn run(self) -> eyre::Result { + let config = load_config_with_root(self.root.clone()); + let paths: Vec = config.project_paths().input_files(); + paths + .par_iter() + .enumerate() + .map(|(i, path)| { + let source = fs::read_to_string(&path)?; + let (mut source_unit, comments) = solang_parser::parse(&source, i) + .map_err(|diags| eyre::eyre!( + "Failed to parse Solidity code for {}. Leaving source unchanged.\nDebug info: {:?}", + path.display(), + diags + ))?; + let mut doc = SolidityDoc::new(comments); + source_unit.visit(&mut doc)?; + + Ok(()) + }) + .collect::>>()?; + + Ok(()) + } +} diff --git a/cli/src/cmd/forge/mod.rs b/cli/src/cmd/forge/mod.rs index b01a3057cf41..17d66f3ecd39 100644 --- a/cli/src/cmd/forge/mod.rs +++ b/cli/src/cmd/forge/mod.rs @@ -44,6 +44,7 @@ pub mod config; pub mod coverage; pub mod create; pub mod debug; +pub mod doc; pub mod flatten; pub mod fmt; pub mod fourbyte; diff --git a/doc/Cargo.toml b/doc/Cargo.toml index f8c126182562..6ebf701da174 100644 --- a/doc/Cargo.toml +++ b/doc/Cargo.toml @@ -1,7 +1,10 @@ [package] -name = "doc" +name = "forge-doc" version = "0.1.0" edition = "2021" +description = """ +Foundry's solidity doc parsing +""" license = "MIT OR Apache-2.0" readme = "README.md" @@ -25,3 +28,4 @@ clap = { version = "3.0.10", features = [ solang-parser = "0.1.17" eyre = "0.6" thiserror = "1.0.30" +mdbook = "0.4.21" diff --git a/doc/src/config.rs b/doc/src/config.rs deleted file mode 100644 index adf01dcb0666..000000000000 --- a/doc/src/config.rs +++ /dev/null @@ -1,7 +0,0 @@ -use std::path::PathBuf; - -#[derive(Debug)] -pub struct DocConfig { - pub templates: Option, - pub output: Option, -} diff --git a/doc/src/context.rs b/doc/src/context.rs deleted file mode 100644 index e1d90abb8e18..000000000000 --- a/doc/src/context.rs +++ /dev/null @@ -1,8 +0,0 @@ -use ethers_solc::ProjectPathsConfig; - -use crate::config::DocConfig; - -pub trait Context { - fn config(&self) -> eyre::Result; - fn project_paths(&self) -> eyre::Result; -} diff --git a/doc/src/lib.rs b/doc/src/lib.rs index 24e8a231e6ce..cd5c21b46541 100644 --- a/doc/src/lib.rs +++ b/doc/src/lib.rs @@ -1,19 +1,21 @@ -use clap::{Parser, ValueHint}; -use config::DocConfig; -use context::Context; -// use ethers_solc::{Graph, Project}; -use thiserror::Error; +use std::{borrow::Borrow, collections::HashMap, path::PathBuf, rc::Rc}; -use forge_fmt::{Formatter, FormatterConfig, Visitable, Visitor}; +use forge_fmt::{Visitable, Visitor}; +use mdbook::{ + book::{Link, Summary, SummaryItem}, + config::{BookConfig, BuildConfig}, + Config, MDBook, +}; +use thiserror::Error; // use foundry_common::paths::ProjectPathsArgs; use solang_parser::{ doccomment::{parse_doccomments, DocComment}, - pt::{Comment, ContractTy, FunctionDefinition, SourceUnit, SourceUnitPart}, + pt::{ + Comment, ContractDefinition, FunctionDefinition, Loc, SourceUnit, SourceUnitPart, + VariableDefinition, + }, }; -mod config; -mod context; - #[derive(Error, Debug)] pub enum DocError {} // TODO: @@ -22,43 +24,59 @@ type Result = std::result::Result; #[derive(Debug)] pub struct SolidityDoc { comments: Vec, - contracts: Vec, + parts: Vec, + start_at: usize, + curr_parent: Option, } -#[derive(Debug)] -pub struct DocContract { - ty: ContractTy, - name: String, - comments: Vec, - // TODO: functions: Vec, - parts: Vec, +#[derive(Debug, PartialEq)] +pub struct SolidityDocPart { + pub comments: Vec, + pub element: SolidityDocPartElement, + pub children: Vec, } -#[derive(Debug)] -pub enum DocContractPart { - Function(DocFunction), -} +impl SolidityDoc { + pub fn new(comments: Vec) -> Self { + SolidityDoc { parts: vec![], comments, start_at: 0, curr_parent: None } + } -#[derive(Debug)] -pub struct DocFunction { - comments: Vec, - name: String, - // args: Vec, - // returns: Vec, -} + fn with_parent( + &mut self, + mut parent: SolidityDocPart, + mut visit: impl FnMut(&mut Self) -> Result<()>, + ) -> Result { + let curr = self.curr_parent.take(); + self.curr_parent = Some(parent); + visit(self)?; + parent = self.curr_parent.take().unwrap(); + self.curr_parent = curr; + Ok(parent) + } -// #[derive(Debug)] -// pub struct DocParam { -// name: Option, -// type_: String, // TODO: -// } + fn add_element_to_parent(&mut self, element: SolidityDocPartElement, loc: Loc) { + let child = + SolidityDocPart { comments: self.parse_docs(loc.start()), element, children: vec![] }; + if let Some(parent) = self.curr_parent.as_mut() { + parent.children.push(child); + } else { + self.parts.push(child); + } + self.start_at = loc.end(); + } -impl SolidityDoc { - pub fn new(comments: Vec) -> Self { - SolidityDoc { contracts: vec![], comments } + fn parse_docs(&mut self, end: usize) -> Vec { + parse_doccomments(&self.comments, self.start_at, end) } } +#[derive(Debug, PartialEq)] +pub enum SolidityDocPartElement { + Contract(Box), + Function(FunctionDefinition), + Variable(VariableDefinition), +} + impl Visitor for SolidityDoc { type Error = DocError; @@ -66,17 +84,34 @@ impl Visitor for SolidityDoc { for source in source_unit.0.iter_mut() { match source { SourceUnitPart::ContractDefinition(def) => { - self.contracts.push(DocContract { - name: def.name.name.clone(), - ty: def.ty.clone(), - comments: parse_doccomments(&self.comments, 0, def.loc.start()), - parts: vec![], - }); - for d in def.parts.iter_mut() { - d.visit(self)?; - } + let contract = SolidityDocPart { + element: SolidityDocPartElement::Contract(def.clone()), + comments: self.parse_docs(def.loc.start()), + children: vec![], + }; + self.start_at = def.loc.start(); + + // StructDefinition(Box), + // EventDefinition(Box), + // EnumDefinition(Box), + // ErrorDefinition(Box), + // VariableDefinition(Box) - done + // FunctionDefinition(Box) - done + // TypeDefinition(Box), + // StraySemicolon(Loc), + // Using(Box), + let contract = self.with_parent(contract, |doc| { + for d in def.parts.iter_mut() { + d.visit(doc)?; + } + + Ok(()) + })?; + + self.start_at = def.loc.end(); + self.parts.push(contract); } - SourceUnitPart::FunctionDefinition(def) => {} + SourceUnitPart::FunctionDefinition(func) => self.visit_function(func)?, _ => {} }; } @@ -85,57 +120,31 @@ impl Visitor for SolidityDoc { } fn visit_function(&mut self, func: &mut FunctionDefinition) -> Result<()> { + self.add_element_to_parent(SolidityDocPartElement::Function(func.clone()), func.loc); Ok(()) } -} -// #[derive(Debug)] -// pub enum Doc { -// Build(DocBuildArgs), -// // Serve, -// } - -// #[derive(Debug, Clone, Parser)] -// pub struct DocBuildArgs { -// #[clap(flatten, next_help_heading = "PROJECT OPTIONS")] -// project_paths: ProjectPathsArgs, -// } - -// impl DocBuildArgs { -// fn run(self) { -// // -// } -// } + fn visit_var_definition(&mut self, var: &mut VariableDefinition) -> Result<()> { + self.add_element_to_parent(SolidityDocPartElement::Variable(var.clone()), var.loc); + Ok(()) + } +} #[cfg(test)] mod tests { use super::*; use std::{fs, path::PathBuf}; - #[allow(non_snake_case)] #[test] - fn test_docs() { - let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata").join("Governor.sol"); + fn test_build() { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("testdata") + .join("oz-governance") + .join("Governor.sol"); let target = fs::read_to_string(path).unwrap(); let (mut source_pt, source_comments) = solang_parser::parse(&target, 1).unwrap(); - // let source_comments = Comments::new(source_comments, source); let mut d = SolidityDoc::new(source_comments); source_pt.visit(&mut d).unwrap(); - // println!("doc {:?}", d); - - for contract in d.contracts { - println!("contract >>> {:?}", contract); - // for comment in contract.comments { - // for comment in comment.comments() { - // println!("comment >>> tag: {} val: {}", comment.tag, comment.value); - // } - // } - // let content = match comment { - // Comment::Block(_, comment) | - // Comment::Line(_, comment) | - // Comment::DocLine(_, comment) | - // Comment::DocBlock(_, comment) => comment, - // }; - } + // write_docs(&d.parts).unwrap(); } } diff --git a/doc/testdata/Governor.sol b/doc/testdata/oz-governance/Governor.sol similarity index 100% rename from doc/testdata/Governor.sol rename to doc/testdata/oz-governance/Governor.sol From 0873dcc8378040290030d31113ec648a9ad2bbb2 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Wed, 10 Aug 2022 21:48:05 +0300 Subject: [PATCH 04/67] fix md gen --- Cargo.lock | 672 ++++----------------------------------- cli/src/cmd/forge/doc.rs | 185 +++++------ cli/src/forge.rs | 3 + cli/src/opts/forge.rs | 4 + doc/Cargo.toml | 1 - doc/src/lib.rs | 18 +- 6 files changed, 144 insertions(+), 739 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ccda0d20ff89..6a4759472622 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,26 +59,13 @@ dependencies = [ "memchr", ] -[[package]] -name = "ammonia" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5ed2509ee88cc023cccee37a6fab35826830fe8b748b3869790e7720c2c4a74" -dependencies = [ - "html5ever", - "maplit", - "once_cell", - "tendril", - "url", -] - [[package]] name = "ansi_term" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" dependencies = [ - "winapi 0.3.9", + "winapi", ] [[package]] @@ -259,7 +246,7 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi", "libc", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -316,10 +303,10 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sha-1 0.10.0", + "sha-1", "sync_wrapper", "tokio", - "tokio-tungstenite 0.17.2", + "tokio-tungstenite", "tower", "tower-http", "tower-layer", @@ -524,9 +511,7 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" dependencies = [ - "lazy_static", "memchr", - "regex-automata", ] [[package]] @@ -691,7 +676,7 @@ dependencies = [ "num-integer", "num-traits", "time 0.1.43", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -763,7 +748,7 @@ dependencies = [ "terminfo", "thiserror", "which", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -882,7 +867,7 @@ checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" dependencies = [ "atty", "lazy_static", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -928,7 +913,7 @@ dependencies = [ "async-trait", "nix 0.22.3", "tokio", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -943,7 +928,7 @@ dependencies = [ "regex", "terminal_size", "unicode-width", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -958,7 +943,7 @@ dependencies = [ "regex", "terminal_size", "unicode-width", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -1071,7 +1056,7 @@ dependencies = [ "parking_lot 0.11.2", "signal-hook", "signal-hook-mio", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -1087,7 +1072,7 @@ dependencies = [ "parking_lot 0.11.2", "signal-hook", "signal-hook-mio", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -1103,7 +1088,7 @@ dependencies = [ "parking_lot 0.12.0", "signal-hook", "signal-hook-mio", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -1112,7 +1097,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a6966607622438301997d3dac0d2f6e9a90c68bb6bc1785ea98456ab93c0507" dependencies = [ - "winapi 0.3.9", + "winapi", ] [[package]] @@ -1121,7 +1106,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" dependencies = [ - "winapi 0.3.9", + "winapi", ] [[package]] @@ -1178,7 +1163,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b37feaa84e6861e00a1f5e5aa8da3ee56d605c9992d33e082786754828e20865" dependencies = [ "nix 0.24.1", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -1191,7 +1176,7 @@ dependencies = [ "libc", "schannel", "socket2", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -1206,7 +1191,7 @@ dependencies = [ "libz-sys", "pkg-config", "vcpkg", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -1303,7 +1288,7 @@ checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" dependencies = [ "libc", "redox_users", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -1314,7 +1299,7 @@ checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", "redox_users", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -1341,18 +1326,6 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" -[[package]] -name = "elasticlunr-rs" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94d9c8df0fe6879ca12e7633fdfe467c503722cc981fc463703472d2b876448" -dependencies = [ - "regex", - "serde", - "serde_derive", - "serde_json", -] - [[package]] name = "elliptic-curve" version = "0.12.2" @@ -1417,19 +1390,6 @@ dependencies = [ "syn", ] -[[package]] -name = "env_logger" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" -dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", -] - [[package]] name = "eth-keystore" version = "0.4.2" @@ -1672,7 +1632,7 @@ dependencies = [ "serde_json", "thiserror", "tokio", - "tokio-tungstenite 0.17.2", + "tokio-tungstenite", "tracing", "tracing-futures", "url", @@ -1865,7 +1825,7 @@ dependencies = [ "cfg-if 1.0.0", "libc", "redox_syscall", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -1952,7 +1912,6 @@ dependencies = [ "eyre", "forge-fmt", "foundry-common", - "mdbook", "solang-parser", "thiserror", ] @@ -2175,7 +2134,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" dependencies = [ "libc", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -2184,25 +2143,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394" -[[package]] -name = "fsevent" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6" -dependencies = [ - "bitflags", - "fsevent-sys 2.0.1", -] - -[[package]] -name = "fsevent-sys" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f41b048a94555da0f42f1d632e2e19510084fb8e303b0daa2816e733fb3644a0" -dependencies = [ - "libc", -] - [[package]] name = "fsevent-sys" version = "4.1.0" @@ -2212,38 +2152,12 @@ dependencies = [ "libc", ] -[[package]] -name = "fuchsia-zircon" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" -dependencies = [ - "bitflags", - "fuchsia-zircon-sys", -] - -[[package]] -name = "fuchsia-zircon-sys" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" - [[package]] name = "funty" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" -[[package]] -name = "futf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" -dependencies = [ - "mac", - "new_debug_unreachable", -] - [[package]] name = "futures" version = "0.3.21" @@ -2469,15 +2383,6 @@ dependencies = [ "url", ] -[[package]] -name = "gitignore" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78aa90e4620c1498ac434c06ba6e521b525794bbdacf085d490cc794b4a2f9a4" -dependencies = [ - "glob", -] - [[package]] name = "glob" version = "0.3.0" @@ -2523,24 +2428,10 @@ dependencies = [ "indexmap", "slab", "tokio", - "tokio-util 0.7.1", + "tokio-util", "tracing", ] -[[package]] -name = "handlebars" -version = "4.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "360d9740069b2f6cbb63ce2dbaa71a20d3185350cbb990d7bebeb9318415eb17" -dependencies = [ - "log", - "pest", - "pest_derive", - "serde", - "serde_json", - "thiserror", -] - [[package]] name = "hash-db" version = "0.15.2" @@ -2581,31 +2472,6 @@ dependencies = [ "fxhash", ] -[[package]] -name = "headers" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d" -dependencies = [ - "base64 0.13.0", - "bitflags", - "bytes", - "headers-core", - "http", - "httpdate", - "mime", - "sha-1 0.10.0", -] - -[[package]] -name = "headers-core" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" -dependencies = [ - "http", -] - [[package]] name = "heck" version = "0.3.3" @@ -2666,21 +2532,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2456aef2e6b6a9784192ae780c0f15bc57df0e918585282325e8c8ac27737654" dependencies = [ - "winapi 0.3.9", -] - -[[package]] -name = "html5ever" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" -dependencies = [ - "log", - "mac", - "markup5ever", - "proc-macro2", - "quote", - "syn", + "winapi", ] [[package]] @@ -2723,12 +2575,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "hyper" version = "0.14.18" @@ -2908,17 +2754,6 @@ version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" -[[package]] -name = "inotify" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4816c66d2c8ae673df83366c18341538f234a26d65a9ecea5c348b453ac1d02f" -dependencies = [ - "bitflags", - "inotify-sys", - "libc", -] - [[package]] name = "inotify" version = "0.9.6" @@ -2951,15 +2786,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "iovec" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" -dependencies = [ - "libc", -] - [[package]] name = "ipnet" version = "2.5.0" @@ -3018,16 +2844,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7" -[[package]] -name = "kernel32-sys" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" -dependencies = [ - "winapi 0.2.8", - "winapi-build", -] - [[package]] name = "kqueue" version = "1.0.5" @@ -3089,12 +2905,6 @@ dependencies = [ "spin", ] -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "libc" version = "0.2.124" @@ -3166,32 +2976,6 @@ dependencies = [ "cfg-if 1.0.0", ] -[[package]] -name = "mac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" - -[[package]] -name = "maplit" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" - -[[package]] -name = "markup5ever" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" -dependencies = [ - "log", - "phf 0.10.1", - "phf_codegen 0.10.0", - "string_cache", - "string_cache_codegen", - "tendril", -] - [[package]] name = "matchers" version = "0.1.0" @@ -3222,39 +3006,6 @@ dependencies = [ "digest 0.10.3", ] -[[package]] -name = "mdbook" -version = "0.4.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23f3e133c6d515528745ffd3b9f0c7d975ae039f0b6abb099f2168daa2afb4f9" -dependencies = [ - "ammonia", - "anyhow", - "chrono 0.4.19", - "clap", - "clap_complete", - "elasticlunr-rs", - "env_logger", - "futures-util", - "gitignore", - "handlebars", - "lazy_static", - "log", - "memchr", - "notify 4.0.17", - "opener", - "pulldown-cmark", - "regex", - "serde", - "serde_json", - "shlex", - "tempfile", - "tokio", - "toml", - "topological-sort", - "warp", -] - [[package]] name = "memchr" version = "2.5.0" @@ -3299,16 +3050,6 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" -[[package]] -name = "mime_guess" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" -dependencies = [ - "mime", - "unicase", -] - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -3324,25 +3065,6 @@ dependencies = [ "adler", ] -[[package]] -name = "mio" -version = "0.6.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" -dependencies = [ - "cfg-if 0.1.10", - "fuchsia-zircon", - "fuchsia-zircon-sys", - "iovec", - "kernel32-sys", - "libc", - "log", - "miow 0.2.2", - "net2", - "slab", - "winapi 0.2.8", -] - [[package]] name = "mio" version = "0.7.14" @@ -3351,9 +3073,9 @@ checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" dependencies = [ "libc", "log", - "miow 0.3.7", + "miow", "ntapi", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -3364,34 +3086,10 @@ checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9" dependencies = [ "libc", "log", - "miow 0.3.7", + "miow", "ntapi", "wasi 0.11.0+wasi-snapshot-preview1", - "winapi 0.3.9", -] - -[[package]] -name = "mio-extras" -version = "2.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19" -dependencies = [ - "lazycell", - "log", - "mio 0.6.23", - "slab", -] - -[[package]] -name = "miow" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" -dependencies = [ - "kernel32-sys", - "net2", - "winapi 0.2.8", - "ws2_32-sys", + "winapi", ] [[package]] @@ -3400,7 +3098,7 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" dependencies = [ - "winapi 0.3.9", + "winapi", ] [[package]] @@ -3421,17 +3119,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "net2" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" -dependencies = [ - "cfg-if 0.1.10", - "libc", - "winapi 0.3.9", -] - [[package]] name = "new_debug_unreachable" version = "1.0.4" @@ -3495,24 +3182,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "notify" -version = "4.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae03c8c853dba7bfd23e571ff0cff7bc9dceb40a4cd684cd1681824183f45257" -dependencies = [ - "bitflags", - "filetime", - "fsevent", - "fsevent-sys 2.0.1", - "inotify 0.7.1", - "libc", - "mio 0.6.23", - "mio-extras", - "walkdir", - "winapi 0.3.9", -] - [[package]] name = "notify" version = "5.0.0-pre.15" @@ -3522,13 +3191,13 @@ dependencies = [ "bitflags", "crossbeam-channel", "filetime", - "fsevent-sys 4.1.0", - "inotify 0.9.6", + "fsevent-sys", + "inotify", "kqueue", "libc", "mio 0.8.2", "walkdir", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -3537,7 +3206,7 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" dependencies = [ - "winapi 0.3.9", + "winapi", ] [[package]] @@ -3700,16 +3369,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" -[[package]] -name = "opener" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea3ebcd72a54701f56345f16785a6d3ac2df7e986d273eb4395c0b01db17952" -dependencies = [ - "bstr", - "winapi 0.3.9", -] - [[package]] name = "openssl" version = "0.10.38" @@ -3755,7 +3414,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66" dependencies = [ - "winapi 0.3.9", + "winapi", ] [[package]] @@ -3822,7 +3481,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -3919,50 +3578,6 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" -[[package]] -name = "pest" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69486e2b8c2d2aeb9762db7b4e00b0331156393555cff467f4163ff06821eef8" -dependencies = [ - "thiserror", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b13570633aff33c6d22ce47dd566b10a3b9122c2fe9d8e7501895905be532b91" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3c567e5702efdc79fb18859ea74c3eb36e14c43da7b8c1f098a4ed6514ec7a0" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pest_meta" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb32be5ee3bbdafa8c7a18b0a8a8d962b66cfa2ceee4037f49267a50ee821fe" -dependencies = [ - "once_cell", - "pest", - "sha-1 0.10.0", -] - [[package]] name = "petgraph" version = "0.5.1" @@ -4013,16 +3628,6 @@ dependencies = [ "phf_shared 0.8.0", ] -[[package]] -name = "phf_codegen" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" -dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", -] - [[package]] name = "phf_generator" version = "0.8.0" @@ -4265,17 +3870,6 @@ version = "2.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf7e6d18738ecd0902d30d1ad232c9125985a3422929b16c65517b38adc14f96" -[[package]] -name = "pulldown-cmark" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d9cc634bc78768157b5cbfe988ffcd1dcba95cd2b2f03a88316c08c6d00ed63" -dependencies = [ - "bitflags", - "memchr", - "unicase", -] - [[package]] name = "pwd" version = "1.3.1" @@ -4485,7 +4079,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" dependencies = [ - "winapi 0.3.9", + "winapi", ] [[package]] @@ -4589,7 +4183,7 @@ dependencies = [ "spin", "untrusted", "web-sys", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -4629,7 +4223,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffc936cf8a7ea60c58f030fd36a612a48f440610214dc54bc36431f9ea0c3efb" dependencies = [ "libc", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -4744,15 +4338,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" dependencies = [ "lazy_static", - "winapi 0.3.9", + "winapi", ] -[[package]] -name = "scoped-tls" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" - [[package]] name = "scopeguard" version = "1.1.0" @@ -4940,19 +4528,6 @@ dependencies = [ "syn", ] -[[package]] -name = "sha-1" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" -dependencies = [ - "block-buffer 0.9.0", - "cfg-if 1.0.0", - "cpufeatures", - "digest 0.9.0", - "opaque-debug 0.3.0", -] - [[package]] name = "sha-1" version = "0.10.0" @@ -5041,12 +4616,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shlex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" - [[package]] name = "signal-hook" version = "0.3.13" @@ -5119,7 +4688,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" dependencies = [ "libc", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -5168,19 +4737,6 @@ dependencies = [ "parking_lot 0.12.0", "phf_shared 0.10.0", "precomputed-hash", - "serde", -] - -[[package]] -name = "string_cache_codegen" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" -dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", - "proc-macro2", - "quote", ] [[package]] @@ -5339,18 +4895,7 @@ dependencies = [ "libc", "redox_syscall", "remove_dir_all", - "winapi 0.3.9", -] - -[[package]] -name = "tendril" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" -dependencies = [ - "futf", - "mac", - "utf-8", + "winapi", ] [[package]] @@ -5361,7 +4906,7 @@ checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" dependencies = [ "dirs-next", "rustversion", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -5380,7 +4925,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" dependencies = [ "libc", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -5393,7 +4938,7 @@ dependencies = [ "fnv", "nom 5.1.2", "phf 0.8.0", - "phf_codegen 0.8.0", + "phf_codegen", ] [[package]] @@ -5442,7 +4987,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" dependencies = [ "libc", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -5504,7 +5049,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -5550,19 +5095,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-tungstenite" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "511de3f85caf1c98983545490c3d09685fa8eb634e57eec22bb4db271f46cbd8" -dependencies = [ - "futures-util", - "log", - "pin-project", - "tokio", - "tungstenite 0.14.0", -] - [[package]] name = "tokio-tungstenite" version = "0.17.2" @@ -5576,25 +5108,11 @@ dependencies = [ "tokio", "tokio-native-tls", "tokio-rustls", - "tungstenite 0.17.3", + "tungstenite", "webpki", "webpki-roots", ] -[[package]] -name = "tokio-util" -version = "0.6.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "log", - "pin-project-lite", - "tokio", -] - [[package]] name = "tokio-util" version = "0.7.1" @@ -5630,12 +5148,6 @@ dependencies = [ "itertools", ] -[[package]] -name = "topological-sort" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa7c7f42dea4b1b99439786f5633aeb9c14c1b53f75e282803c2ec2ad545873c" - [[package]] name = "tower" version = "0.4.12" @@ -5647,7 +5159,7 @@ dependencies = [ "pin-project", "pin-project-lite", "tokio", - "tokio-util 0.7.1", + "tokio-util", "tower-layer", "tower-service", "tracing", @@ -5812,25 +5324,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "tungstenite" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0b2d8558abd2e276b0a8df5c05a2ec762609344191e5fd23e292c910e9165b5" -dependencies = [ - "base64 0.13.0", - "byteorder", - "bytes", - "http", - "httparse", - "log", - "rand 0.8.5", - "sha-1 0.9.8", - "thiserror", - "url", - "utf-8", -] - [[package]] name = "tungstenite" version = "0.17.3" @@ -5846,7 +5339,7 @@ dependencies = [ "native-tls", "rand 0.8.5", "rustls", - "sha-1 0.10.0", + "sha-1", "thiserror", "url", "utf-8", @@ -5859,12 +5352,6 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" -[[package]] -name = "ucd-trie" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89570599c4fe5585de2b388aab47e99f7fa4e9238a1399f707a02e356058141c" - [[package]] name = "ui" version = "0.2.0" @@ -6044,7 +5531,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" dependencies = [ "same-file", - "winapi 0.3.9", + "winapi", "winapi-util", ] @@ -6058,35 +5545,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "warp" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cef4e1e9114a4b7f1ac799f16ce71c14de5778500c5450ec6b7b920c55b587e" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "headers", - "http", - "hyper", - "log", - "mime", - "mime_guess", - "percent-encoding", - "pin-project", - "scoped-tls", - "serde", - "serde_json", - "serde_urlencoded", - "tokio", - "tokio-stream", - "tokio-tungstenite 0.15.0", - "tokio-util 0.6.10", - "tower-service", - "tracing", -] - [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" @@ -6202,7 +5660,7 @@ dependencies = [ "ignore-files", "libc", "miette", - "notify 5.0.0-pre.15", + "notify", "once_cell", "project-origins", "thiserror", @@ -6250,12 +5708,6 @@ dependencies = [ "libc", ] -[[package]] -name = "winapi" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" - [[package]] name = "winapi" version = "0.3.9" @@ -6266,12 +5718,6 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu", ] -[[package]] -name = "winapi-build" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" - [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" @@ -6284,7 +5730,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ - "winapi 0.3.9", + "winapi", ] [[package]] @@ -6342,17 +5788,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" dependencies = [ - "winapi 0.3.9", -] - -[[package]] -name = "ws2_32-sys" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" -dependencies = [ - "winapi 0.2.8", - "winapi-build", + "winapi", ] [[package]] diff --git a/cli/src/cmd/forge/doc.rs b/cli/src/cmd/forge/doc.rs index 44bb1f4bdfab..7338ef3af154 100644 --- a/cli/src/cmd/forge/doc.rs +++ b/cli/src/cmd/forge/doc.rs @@ -1,11 +1,14 @@ use crate::cmd::Cmd; use clap::{Parser, ValueHint}; -use forge_doc::{SolidityDoc, SolidityDocPart, SolidityDocPartElement}; +use forge_doc::{SolidityDoc, SolidityDocPartElement}; use forge_fmt::Visitable; -use foundry_config::load_config_with_root; +use foundry_config::{find_project_root_path, load_config_with_root}; use rayon::prelude::*; use solang_parser::doccomment::DocComment; -use std::{fs, path::PathBuf}; +use std::{ + fs, + path::{Path, PathBuf}, +}; #[derive(Debug, Clone, Parser)] pub struct DocArgs { @@ -24,114 +27,16 @@ impl DocArgs { comments .iter() .map(|c| match c { - DocComment::Line { comment } => { - // format!("Tag: {}. {}", comment.tag, comment.value) - format!("{}", comment.value) - } + DocComment::Line { comment } => comment.value.to_owned(), DocComment::Block { comments } => comments .iter() - .map(|comment| - // format!("Tag: {}. {}", comment.tag, comment.value) - format!("{}", comment.value)) + .map(|comment| comment.value.to_owned()) .collect::>() .join("\n"), }) .collect::>() .join("\n") } - - pub fn write_docs(&self, parts: &Vec) -> Result<(), eyre::Error> { - // TODO: - let out_dir = - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("testdata").join("oz-governance"); - std::fs::create_dir_all(&out_dir.join("docs"))?; - for part in parts.iter() { - if let SolidityDocPartElement::Contract(ref contract) = part.element { - let mut out = vec![ - format!("# {}", contract.name), - "".to_owned(), - self.format_comments(&part.comments), - "".to_owned(), - ]; - - let mut attributes = vec![]; - let mut funcs = vec![]; - - for child in part.children.iter() { - match &child.element { - SolidityDocPartElement::Function(func) => { - funcs.push((func.clone(), child.comments.clone())) - } - SolidityDocPartElement::Variable(var) => { - attributes.push((var.clone(), child.comments.clone())) - } - _ => {} - } - } - - out.push("## Attributes".to_owned()); - out.extend(attributes.iter().flat_map(|(var, comments)| { - vec![ - format!("### {}", var.name.name), - self.format_comments(&comments), - "".to_owned(), - ] - })); - - out.push("## Functions".to_owned()); - out.extend(funcs.iter().flat_map(|(func, comments)| { - vec![ - format!( - "### {}", - func.name.as_ref().map_or(func.ty.to_string(), |n| n.name.to_owned()) - ), - self.format_comments(&comments), - "".to_owned(), - ] - })); - - std::fs::write(out_dir.join("docs").join("Governor.md"), out.join("\n"))?; - } - - std::fs::write( - out_dir.join("docs").join("SUMMARY.md"), - "# OZ Governance\n - [Governor](Governor.md)", - )?; - std::fs::write( - out_dir.join("book.toml"), - "[book]\ntitle = \"OZ Governance\"\nsrc = \"docs\"", - )?; - } - // let title = "Test"; - // let summary: Summary = Summary { - // title: Some(title.to_owned()), - // prefix_chapters: vec![], - // numbered_chapters: vec![ - // SummaryItem::PartTitle("Contracts".to_owned()), - // SummaryItem::Link(Link::new("Governor2", - // src_dir.join("src").join("Governor.md"))), ], - // suffix_chapters: vec![], - // }; - // let mut config = Config::default(); - // config.book = BookConfig { - // title: Some(title.to_owned()), - // language: Some("solidity".to_owned()), - // authors: vec![], - // description: None, - // multilingual: false, - // src: src_dir.clone(), - // }; - // config.build = BuildConfig { - // build_dir: src_dir.join("book"), - // create_missing: false, - // use_default_preprocessors: true, - // }; - // MDBook::load_with_config_and_summary(env!("CARGO_MANIFEST_DIR"), config, summary) - // .unwrap() - // .build() - // .unwrap(); - Ok(()) - } } impl Cmd for DocArgs { @@ -140,7 +45,7 @@ impl Cmd for DocArgs { fn run(self) -> eyre::Result { let config = load_config_with_root(self.root.clone()); let paths: Vec = config.project_paths().input_files(); - paths + let docs = paths .par_iter() .enumerate() .map(|(i, path)| { @@ -154,10 +59,80 @@ impl Cmd for DocArgs { let mut doc = SolidityDoc::new(comments); source_unit.visit(&mut doc)?; - Ok(()) + Ok((path.clone(), doc.parts)) }) .collect::>>()?; + let out_dir = self.root.as_ref().unwrap_or(&find_project_root_path()?).join("docs"); + fs::create_dir_all(&out_dir.join("src"))?; + + let mut filenames = vec![]; + for (path, doc) in docs.iter() { + for part in doc.iter() { + if let SolidityDocPartElement::Contract(ref contract) = part.element { + let mut out = vec![ + format!("# {}", contract.name), + "".to_owned(), + self.format_comments(&part.comments), + "".to_owned(), + ]; + + let mut attributes = vec![]; + let mut funcs = vec![]; + + for child in part.children.iter() { + match &child.element { + SolidityDocPartElement::Function(func) => { + funcs.push((func.clone(), child.comments.clone())) + } + SolidityDocPartElement::Variable(var) => { + attributes.push((var.clone(), child.comments.clone())) + } + _ => {} + } + } + + out.push("## Attributes".to_owned()); + out.extend(attributes.iter().flat_map(|(var, comments)| { + vec![ + format!("### {}", var.name.name), + self.format_comments(&comments), + "".to_owned(), + ] + })); + + out.push("## Functions".to_owned()); + out.extend(funcs.iter().flat_map(|(func, comments)| { + vec![ + format!( + "### {}", + func.name + .as_ref() + .map_or(func.ty.to_string(), |n| n.name.to_owned()) + ), + self.format_comments(&comments), + "".to_owned(), + ] + })); + + let src_filename = Path::new(path).file_stem().unwrap().to_str().unwrap(); + let filename = format!("{}.md", src_filename); + std::fs::write(out_dir.join("src").join(&filename), out.join("\n"))?; + filenames.push((src_filename, filename)); + } + } + } + let mut summary = String::from("# Summary\n"); + summary.push_str( + &filenames + .iter() + .map(|(src, filename)| format!("- [{}]({})", src, filename)) + .collect::>() + .join("\n"), + ); + std::fs::write(out_dir.join("src").join("SUMMARY.md"), summary)?; + std::fs::write(out_dir.join("book.toml"), "[book]\ntitle = \"OZ\"\nsrc = \"src\"")?; + Ok(()) } } diff --git a/cli/src/forge.rs b/cli/src/forge.rs index d4390d9343f7..f3587df11934 100644 --- a/cli/src/forge.rs +++ b/cli/src/forge.rs @@ -126,6 +126,9 @@ fn main() -> eyre::Result<()> { Subcommands::Tree(cmd) => { cmd.run()?; } + Subcommands::Doc(cmd) => { + cmd.run()?; + } } Ok(()) diff --git a/cli/src/opts/forge.rs b/cli/src/opts/forge.rs index a602afec483a..74c67eee07b2 100644 --- a/cli/src/opts/forge.rs +++ b/cli/src/opts/forge.rs @@ -10,6 +10,7 @@ use crate::cmd::forge::{ config, coverage, create::CreateArgs, debug::DebugArgs, + doc::DocArgs, flatten, fmt::FmtArgs, fourbyte::UploadSelectorsArgs, @@ -167,6 +168,9 @@ pub enum Subcommands { about = "Display a tree visualization of the project's dependency graph." )] Tree(tree::TreeArgs), + + #[clap(about = "Generate documentation for the project,")] + Doc(DocArgs), } // A set of solc compiler settings that can be set via command line arguments, which are intended diff --git a/doc/Cargo.toml b/doc/Cargo.toml index 6ebf701da174..4b0f4ea7dc58 100644 --- a/doc/Cargo.toml +++ b/doc/Cargo.toml @@ -28,4 +28,3 @@ clap = { version = "3.0.10", features = [ solang-parser = "0.1.17" eyre = "0.6" thiserror = "1.0.30" -mdbook = "0.4.21" diff --git a/doc/src/lib.rs b/doc/src/lib.rs index cd5c21b46541..a0303276043a 100644 --- a/doc/src/lib.rs +++ b/doc/src/lib.rs @@ -6,8 +6,6 @@ use mdbook::{ config::{BookConfig, BuildConfig}, Config, MDBook, }; -use thiserror::Error; -// use foundry_common::paths::ProjectPathsArgs; use solang_parser::{ doccomment::{parse_doccomments, DocComment}, pt::{ @@ -15,6 +13,7 @@ use solang_parser::{ VariableDefinition, }, }; +use thiserror::Error; #[derive(Error, Debug)] pub enum DocError {} // TODO: @@ -23,8 +22,8 @@ type Result = std::result::Result; #[derive(Debug)] pub struct SolidityDoc { + pub parts: Vec, comments: Vec, - parts: Vec, start_at: usize, curr_parent: Option, } @@ -90,16 +89,6 @@ impl Visitor for SolidityDoc { children: vec![], }; self.start_at = def.loc.start(); - - // StructDefinition(Box), - // EventDefinition(Box), - // EnumDefinition(Box), - // ErrorDefinition(Box), - // VariableDefinition(Box) - done - // FunctionDefinition(Box) - done - // TypeDefinition(Box), - // StraySemicolon(Loc), - // Using(Box), let contract = self.with_parent(contract, |doc| { for d in def.parts.iter_mut() { d.visit(doc)?; @@ -144,7 +133,6 @@ mod tests { let target = fs::read_to_string(path).unwrap(); let (mut source_pt, source_comments) = solang_parser::parse(&target, 1).unwrap(); let mut d = SolidityDoc::new(source_comments); - source_pt.visit(&mut d).unwrap(); - // write_docs(&d.parts).unwrap(); + assert!(source_pt.visit(&mut d).is_ok()); } } From ca8a2b9c5048a549e28959472a1bd191efa8dcc7 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Wed, 10 Aug 2022 21:52:24 +0300 Subject: [PATCH 05/67] fix --- doc/src/lib.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/doc/src/lib.rs b/doc/src/lib.rs index a0303276043a..ede5c9833f54 100644 --- a/doc/src/lib.rs +++ b/doc/src/lib.rs @@ -1,11 +1,4 @@ -use std::{borrow::Borrow, collections::HashMap, path::PathBuf, rc::Rc}; - use forge_fmt::{Visitable, Visitor}; -use mdbook::{ - book::{Link, Summary, SummaryItem}, - config::{BookConfig, BuildConfig}, - Config, MDBook, -}; use solang_parser::{ doccomment::{parse_doccomments, DocComment}, pt::{ @@ -125,7 +118,7 @@ mod tests { use std::{fs, path::PathBuf}; #[test] - fn test_build() { + fn parse_docs() { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("testdata") .join("oz-governance") From be2834839f375a57055ef864705c2ae32064a45f Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Sun, 14 Aug 2022 14:01:25 +0300 Subject: [PATCH 06/67] change doc layout & extract to builder --- Cargo.lock | 1 + cli/src/cmd/forge/doc.rs | 120 ++------------------------------------- doc/Cargo.toml | 1 + doc/src/builder.rs | 120 +++++++++++++++++++++++++++++++++++++++ doc/src/doc_format.rs | 40 +++++++++++++ doc/src/lib.rs | 3 + 6 files changed, 170 insertions(+), 115 deletions(-) create mode 100644 doc/src/builder.rs create mode 100644 doc/src/doc_format.rs diff --git a/Cargo.lock b/Cargo.lock index 6a4759472622..1fd625ef2e42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1912,6 +1912,7 @@ dependencies = [ "eyre", "forge-fmt", "foundry-common", + "rayon", "solang-parser", "thiserror", ] diff --git a/cli/src/cmd/forge/doc.rs b/cli/src/cmd/forge/doc.rs index 7338ef3af154..6d11ee5eace7 100644 --- a/cli/src/cmd/forge/doc.rs +++ b/cli/src/cmd/forge/doc.rs @@ -1,14 +1,8 @@ use crate::cmd::Cmd; use clap::{Parser, ValueHint}; -use forge_doc::{SolidityDoc, SolidityDocPartElement}; -use forge_fmt::Visitable; +use forge_doc::builder::DocBuilder; use foundry_config::{find_project_root_path, load_config_with_root}; -use rayon::prelude::*; -use solang_parser::doccomment::DocComment; -use std::{ - fs, - path::{Path, PathBuf}, -}; +use std::path::PathBuf; #[derive(Debug, Clone, Parser)] pub struct DocArgs { @@ -22,117 +16,13 @@ pub struct DocArgs { root: Option, } -impl DocArgs { - fn format_comments(&self, comments: &Vec) -> String { - comments - .iter() - .map(|c| match c { - DocComment::Line { comment } => comment.value.to_owned(), - DocComment::Block { comments } => comments - .iter() - .map(|comment| comment.value.to_owned()) - .collect::>() - .join("\n"), - }) - .collect::>() - .join("\n") - } -} - impl Cmd for DocArgs { type Output = (); fn run(self) -> eyre::Result { let config = load_config_with_root(self.root.clone()); - let paths: Vec = config.project_paths().input_files(); - let docs = paths - .par_iter() - .enumerate() - .map(|(i, path)| { - let source = fs::read_to_string(&path)?; - let (mut source_unit, comments) = solang_parser::parse(&source, i) - .map_err(|diags| eyre::eyre!( - "Failed to parse Solidity code for {}. Leaving source unchanged.\nDebug info: {:?}", - path.display(), - diags - ))?; - let mut doc = SolidityDoc::new(comments); - source_unit.visit(&mut doc)?; - - Ok((path.clone(), doc.parts)) - }) - .collect::>>()?; - - let out_dir = self.root.as_ref().unwrap_or(&find_project_root_path()?).join("docs"); - fs::create_dir_all(&out_dir.join("src"))?; - - let mut filenames = vec![]; - for (path, doc) in docs.iter() { - for part in doc.iter() { - if let SolidityDocPartElement::Contract(ref contract) = part.element { - let mut out = vec![ - format!("# {}", contract.name), - "".to_owned(), - self.format_comments(&part.comments), - "".to_owned(), - ]; - - let mut attributes = vec![]; - let mut funcs = vec![]; - - for child in part.children.iter() { - match &child.element { - SolidityDocPartElement::Function(func) => { - funcs.push((func.clone(), child.comments.clone())) - } - SolidityDocPartElement::Variable(var) => { - attributes.push((var.clone(), child.comments.clone())) - } - _ => {} - } - } - - out.push("## Attributes".to_owned()); - out.extend(attributes.iter().flat_map(|(var, comments)| { - vec![ - format!("### {}", var.name.name), - self.format_comments(&comments), - "".to_owned(), - ] - })); - - out.push("## Functions".to_owned()); - out.extend(funcs.iter().flat_map(|(func, comments)| { - vec![ - format!( - "### {}", - func.name - .as_ref() - .map_or(func.ty.to_string(), |n| n.name.to_owned()) - ), - self.format_comments(&comments), - "".to_owned(), - ] - })); - - let src_filename = Path::new(path).file_stem().unwrap().to_str().unwrap(); - let filename = format!("{}.md", src_filename); - std::fs::write(out_dir.join("src").join(&filename), out.join("\n"))?; - filenames.push((src_filename, filename)); - } - } - } - let mut summary = String::from("# Summary\n"); - summary.push_str( - &filenames - .iter() - .map(|(src, filename)| format!("- [{}]({})", src, filename)) - .collect::>() - .join("\n"), - ); - std::fs::write(out_dir.join("src").join("SUMMARY.md"), summary)?; - std::fs::write(out_dir.join("book.toml"), "[book]\ntitle = \"OZ\"\nsrc = \"src\"")?; - - Ok(()) + DocBuilder::new(self.root.as_ref().unwrap_or(&find_project_root_path()?)) + .with_paths(config.project_paths().input_files()) + .build() } } diff --git a/doc/Cargo.toml b/doc/Cargo.toml index 4b0f4ea7dc58..7ca9b07d5916 100644 --- a/doc/Cargo.toml +++ b/doc/Cargo.toml @@ -28,3 +28,4 @@ clap = { version = "3.0.10", features = [ solang-parser = "0.1.17" eyre = "0.6" thiserror = "1.0.30" +rayon = "1.5.1" diff --git a/doc/src/builder.rs b/doc/src/builder.rs new file mode 100644 index 000000000000..bc5c3139c434 --- /dev/null +++ b/doc/src/builder.rs @@ -0,0 +1,120 @@ +use eyre; +use forge_fmt::Visitable; +use rayon::prelude::*; +use std::{ + fmt::Write, + fs, + path::{Path, PathBuf}, +}; + +use crate::{doc_format::DocFormat, SolidityDoc, SolidityDocPartElement}; + +pub struct DocBuilder { + root: PathBuf, + paths: Vec, +} + +impl DocBuilder { + pub fn new(root: &Path) -> Self { + DocBuilder { root: root.to_owned(), paths: vec![] } + } + + pub fn with_paths(mut self, paths: Vec) -> Self { + self.paths = paths; + self + } + + pub fn build(self) -> eyre::Result<()> { + let docs = self + .paths + .par_iter() + .enumerate() + .map(|(i, path)| { + let source = fs::read_to_string(&path)?; + let (mut source_unit, comments) = + solang_parser::parse(&source, i).map_err(|diags| { + eyre::eyre!( + "Failed to parse Solidity code for {}\nDebug info: {:?}", + path.display(), + diags + ) + })?; + let mut doc = SolidityDoc::new(comments); + source_unit.visit(&mut doc)?; + + Ok((path.clone(), doc.parts)) + }) + .collect::>>()?; + + let out_dir = self.root.join("docs"); + let out_dir_src = out_dir.join("src"); + fs::create_dir_all(&out_dir_src)?; + + let mut filenames = vec![]; + for (path, doc) in docs.iter() { + println!("processing {}", path.display()); + for part in doc.iter() { + if let SolidityDocPartElement::Contract(ref contract) = part.element { + println!("\twriting {}", contract.name); + let mut out = vec![ + format!("# {}", contract.name), + "".to_owned(), + part.comments.doc(), + "".to_owned(), + ]; + + let mut attributes = vec![]; + let mut funcs = vec![]; + + for child in part.children.iter() { + match &child.element { + SolidityDocPartElement::Function(func) => { + funcs.push((func.clone(), child.comments.clone())) + } + SolidityDocPartElement::Variable(var) => { + attributes.push((var.clone(), child.comments.clone())) + } + _ => {} + } + } + + if !attributes.is_empty() { + out.push("## Attributes".to_owned()); + out.extend(attributes.iter().flat_map(|(var, comments)| { + vec![var.doc(), comments.doc(), "".to_owned()] + })); + } + + if !funcs.is_empty() { + out.push("## Functions".to_owned()); + out.extend(funcs.iter().flat_map(|(func, comments)| { + vec![func.doc(), comments.doc(), "".to_owned()] + })); + } + + let filename = format!("contract.{}.md", contract.name); + let mut new_path = path.strip_prefix(&self.root)?.to_owned(); + new_path.pop(); + fs::create_dir_all(out_dir_src.join(&new_path))?; + let new_path = new_path.join(&filename); + fs::write(out_dir_src.join(&new_path), out.join("\n"))?; + filenames.push((contract.name.clone(), new_path)); + } + } + } + let mut summary = String::new(); + write!( + summary, + "# Summary\n{}", + &filenames + .iter() + .map(|(src, filename)| format!("- [{}]({})", src, filename.display())) + .collect::>() + .join("\n") + )?; + fs::write(out_dir_src.join("SUMMARY.md"), summary)?; + // TODO: take values from config + fs::write(out_dir.join("book.toml"), "[book]\ntitle = \"OZ\"\nsrc = \"src\"")?; + Ok(()) + } +} diff --git a/doc/src/doc_format.rs b/doc/src/doc_format.rs new file mode 100644 index 000000000000..66b2c12597b3 --- /dev/null +++ b/doc/src/doc_format.rs @@ -0,0 +1,40 @@ +use solang_parser::{ + doccomment::DocComment, + pt::{FunctionDefinition, VariableDefinition}, +}; + +pub trait DocFormat { + fn doc(&self) -> String; +} + +impl DocFormat for DocComment { + fn doc(&self) -> String { + match self { + DocComment::Line { comment } => comment.value.to_owned(), + DocComment::Block { comments } => comments + .iter() + .map(|comment| comment.value.to_owned()) + .collect::>() + .join("\n"), + } + } +} + +impl DocFormat for Vec { + fn doc(&self) -> String { + self.iter().map(DocComment::doc).collect::>().join("\n") + } +} + +impl DocFormat for VariableDefinition { + fn doc(&self) -> String { + format!("### {}", self.name.name) + } +} + +impl DocFormat for FunctionDefinition { + fn doc(&self) -> String { + let name = self.name.as_ref().map_or(self.ty.to_string(), |n| n.name.to_owned()); + format!("### {}\n{}", name, self.ty) + } +} diff --git a/doc/src/lib.rs b/doc/src/lib.rs index ede5c9833f54..cc811d13912d 100644 --- a/doc/src/lib.rs +++ b/doc/src/lib.rs @@ -8,6 +8,9 @@ use solang_parser::{ }; use thiserror::Error; +pub mod builder; +mod doc_format; + #[derive(Error, Debug)] pub enum DocError {} // TODO: From 3ace756d31b5612027bb35f264b18edf621de59d Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Sun, 14 Aug 2022 19:52:50 +0300 Subject: [PATCH 07/67] misc --- Cargo.lock | 1 + doc/Cargo.toml | 1 + doc/src/builder.rs | 94 ++++++++++++++++++++++++++++++---------------- doc/src/lib.rs | 19 +++++----- doc/src/macros.rs | 10 +++++ 5 files changed, 83 insertions(+), 42 deletions(-) create mode 100644 doc/src/macros.rs diff --git a/Cargo.lock b/Cargo.lock index 1fd625ef2e42..aad2131ab331 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1912,6 +1912,7 @@ dependencies = [ "eyre", "forge-fmt", "foundry-common", + "itertools", "rayon", "solang-parser", "thiserror", diff --git a/doc/Cargo.toml b/doc/Cargo.toml index 7ca9b07d5916..a040d550be9b 100644 --- a/doc/Cargo.toml +++ b/doc/Cargo.toml @@ -29,3 +29,4 @@ solang-parser = "0.1.17" eyre = "0.6" thiserror = "1.0.30" rayon = "1.5.1" +itertools = "0.10.3" diff --git a/doc/src/builder.rs b/doc/src/builder.rs index bc5c3139c434..3eeead267f76 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -1,22 +1,24 @@ use eyre; use forge_fmt::Visitable; use rayon::prelude::*; +use solang_parser::pt::{ContractTy, Identifier}; use std::{ fmt::Write, fs, path::{Path, PathBuf}, }; -use crate::{doc_format::DocFormat, SolidityDoc, SolidityDocPartElement}; +use crate::{doc_format::DocFormat, macros::*, SolidityDoc, SolidityDocPartElement}; pub struct DocBuilder { root: PathBuf, + out_dir: PathBuf, paths: Vec, } impl DocBuilder { pub fn new(root: &Path) -> Self { - DocBuilder { root: root.to_owned(), paths: vec![] } + DocBuilder { root: root.to_owned(), out_dir: PathBuf::from("docs"), paths: vec![] } } pub fn with_paths(mut self, paths: Vec) -> Self { @@ -24,6 +26,11 @@ impl DocBuilder { self } + pub fn with_out_dir(mut self, out_dir: &Path) -> Self { + self.out_dir = out_dir.to_owned(); + self + } + pub fn build(self) -> eyre::Result<()> { let docs = self .paths @@ -46,22 +53,17 @@ impl DocBuilder { }) .collect::>>()?; - let out_dir = self.root.join("docs"); + let out_dir = self.root.join("docs"); // TODO: config let out_dir_src = out_dir.join("src"); fs::create_dir_all(&out_dir_src)?; let mut filenames = vec![]; for (path, doc) in docs.iter() { - println!("processing {}", path.display()); for part in doc.iter() { if let SolidityDocPartElement::Contract(ref contract) = part.element { - println!("\twriting {}", contract.name); - let mut out = vec![ - format!("# {}", contract.name), - "".to_owned(), - part.comments.doc(), - "".to_owned(), - ]; + let mut doc_file = String::new(); + writeln!(doc_file, "# {}", contract.name)?; + writeln_doc!(doc_file, part.comments)?; let mut attributes = vec![]; let mut funcs = vec![]; @@ -79,42 +81,68 @@ impl DocBuilder { } if !attributes.is_empty() { - out.push("## Attributes".to_owned()); - out.extend(attributes.iter().flat_map(|(var, comments)| { - vec![var.doc(), comments.doc(), "".to_owned()] - })); + writeln!(doc_file, "## Attributes")?; + for (var, comments) in attributes { + writeln_doc!(doc_file, "{}{}\n", var, comments)?; + } } if !funcs.is_empty() { - out.push("## Functions".to_owned()); - out.extend(funcs.iter().flat_map(|(func, comments)| { - vec![func.doc(), comments.doc(), "".to_owned()] - })); + writeln!(doc_file, "## Functions")?; + for (func, comments) in funcs { + writeln_doc!(doc_file, "{}{}\n", func, comments)?; + } } - let filename = format!("contract.{}.md", contract.name); let mut new_path = path.strip_prefix(&self.root)?.to_owned(); new_path.pop(); fs::create_dir_all(out_dir_src.join(&new_path))?; - let new_path = new_path.join(&filename); - fs::write(out_dir_src.join(&new_path), out.join("\n"))?; - filenames.push((contract.name.clone(), new_path)); + let new_path = new_path.join(&format!("contract.{}.md", contract.name)); + + fs::write(out_dir_src.join(&new_path), doc_file)?; + filenames.push((contract.name.clone(), contract.ty.clone(), new_path)); } } } + let mut summary = String::new(); - write!( - summary, - "# Summary\n{}", - &filenames - .iter() - .map(|(src, filename)| format!("- [{}]({})", src, filename.display())) - .collect::>() - .join("\n") - )?; + writeln!(summary, "# Summary")?; + + // TODO: + let mut interfaces = vec![]; + let mut contracts = vec![]; + let mut abstract_contracts = vec![]; + let mut libraries = vec![]; + for entry in filenames.into_iter() { + match entry.1 { + ContractTy::Abstract(_) => abstract_contracts.push(entry), + ContractTy::Contract(_) => contracts.push(entry), + ContractTy::Interface(_) => interfaces.push(entry), + ContractTy::Library(_) => libraries.push(entry), + } + } + + let mut write_section = |title: &str, + entries: &[(Identifier, ContractTy, PathBuf)]| + -> Result<(), std::fmt::Error> { + if !entries.is_empty() { + writeln!(summary, "# {title}")?; + for (src, _, filename) in entries { + writeln!(summary, "- [{}]({})", src, filename.display())?; + } + writeln!(summary)?; + } + Ok(()) + }; + + write_section("Contracts", &contracts)?; + write_section("Libraries", &libraries)?; + write_section("Interfaces", &interfaces)?; + write_section("Abstract Contracts", &abstract_contracts)?; + fs::write(out_dir_src.join("SUMMARY.md"), summary)?; // TODO: take values from config - fs::write(out_dir.join("book.toml"), "[book]\ntitle = \"OZ\"\nsrc = \"src\"")?; + fs::write(out_dir.join("book.toml"), "[book]\ntitle = \"Title\"\nsrc = \"src\"")?; Ok(()) } } diff --git a/doc/src/lib.rs b/doc/src/lib.rs index cc811d13912d..88e52fc278ae 100644 --- a/doc/src/lib.rs +++ b/doc/src/lib.rs @@ -10,29 +10,30 @@ use thiserror::Error; pub mod builder; mod doc_format; +mod macros; #[derive(Error, Debug)] -pub enum DocError {} // TODO: +enum DocError {} // TODO: type Result = std::result::Result; #[derive(Debug)] -pub struct SolidityDoc { - pub parts: Vec, +struct SolidityDoc { + parts: Vec, comments: Vec, start_at: usize, curr_parent: Option, } #[derive(Debug, PartialEq)] -pub struct SolidityDocPart { - pub comments: Vec, - pub element: SolidityDocPartElement, - pub children: Vec, +struct SolidityDocPart { + comments: Vec, + element: SolidityDocPartElement, + children: Vec, } impl SolidityDoc { - pub fn new(comments: Vec) -> Self { + fn new(comments: Vec) -> Self { SolidityDoc { parts: vec![], comments, start_at: 0, curr_parent: None } } @@ -66,7 +67,7 @@ impl SolidityDoc { } #[derive(Debug, PartialEq)] -pub enum SolidityDocPartElement { +enum SolidityDocPartElement { Contract(Box), Function(FunctionDefinition), Variable(VariableDefinition), diff --git a/doc/src/macros.rs b/doc/src/macros.rs new file mode 100644 index 000000000000..4617afa0c65e --- /dev/null +++ b/doc/src/macros.rs @@ -0,0 +1,10 @@ +macro_rules! writeln_doc { + ($dst:expr, $arg:expr) => { + writeln_doc!($dst, "{}", $arg) + }; + ($dst:expr, $format:literal, $($arg:expr),*) => { + writeln!($dst, "{}", format_args!($format, $($arg.doc(),)*)) + }; +} + +pub(crate) use writeln_doc; From e3ea12901eeda087b6aeb404525f9a20f8c8d6ca Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Sun, 14 Aug 2022 22:19:36 +0300 Subject: [PATCH 08/67] output format --- doc/src/builder.rs | 27 +++++++++++++----- doc/src/doc_format.rs | 40 -------------------------- doc/src/format.rs | 65 +++++++++++++++++++++++++++++++++++++++++++ doc/src/lib.rs | 3 +- doc/src/output.rs | 15 ++++++++++ 5 files changed, 102 insertions(+), 48 deletions(-) delete mode 100644 doc/src/doc_format.rs create mode 100644 doc/src/format.rs create mode 100644 doc/src/output.rs diff --git a/doc/src/builder.rs b/doc/src/builder.rs index 3eeead267f76..fa69868ab3de 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -8,7 +8,7 @@ use std::{ path::{Path, PathBuf}, }; -use crate::{doc_format::DocFormat, macros::*, SolidityDoc, SolidityDocPartElement}; +use crate::{format::DocFormat, macros::*, output::DocOutput, SolidityDoc, SolidityDocPartElement}; pub struct DocBuilder { root: PathBuf, @@ -62,7 +62,16 @@ impl DocBuilder { for part in doc.iter() { if let SolidityDocPartElement::Contract(ref contract) = part.element { let mut doc_file = String::new(); - writeln!(doc_file, "# {}", contract.name)?; + writeln_doc!(doc_file, DocOutput::H1(&contract.name.name))?; + if !contract.base.is_empty() { + writeln_doc!( + doc_file, + "{} {}\n", + DocOutput::Bold("Inherits:"), + contract.base + )?; + } + writeln_doc!(doc_file, part.comments)?; let mut attributes = vec![]; @@ -81,14 +90,14 @@ impl DocBuilder { } if !attributes.is_empty() { - writeln!(doc_file, "## Attributes")?; + writeln_doc!(doc_file, DocOutput::H2("Attributes"))?; for (var, comments) in attributes { writeln_doc!(doc_file, "{}{}\n", var, comments)?; } } if !funcs.is_empty() { - writeln!(doc_file, "## Functions")?; + writeln_doc!(doc_file, DocOutput::H2("Functions"))?; for (func, comments) in funcs { writeln_doc!(doc_file, "{}{}\n", func, comments)?; } @@ -106,7 +115,7 @@ impl DocBuilder { } let mut summary = String::new(); - writeln!(summary, "# Summary")?; + writeln_doc!(summary, DocOutput::H1("Summary"))?; // TODO: let mut interfaces = vec![]; @@ -126,9 +135,13 @@ impl DocBuilder { entries: &[(Identifier, ContractTy, PathBuf)]| -> Result<(), std::fmt::Error> { if !entries.is_empty() { - writeln!(summary, "# {title}")?; + writeln_doc!(summary, DocOutput::H1(title))?; for (src, _, filename) in entries { - writeln!(summary, "- [{}]({})", src, filename.display())?; + writeln_doc!( + summary, + "- {}", + DocOutput::Link(&src.name, &filename.display().to_string()) + )?; } writeln!(summary)?; } diff --git a/doc/src/doc_format.rs b/doc/src/doc_format.rs deleted file mode 100644 index 66b2c12597b3..000000000000 --- a/doc/src/doc_format.rs +++ /dev/null @@ -1,40 +0,0 @@ -use solang_parser::{ - doccomment::DocComment, - pt::{FunctionDefinition, VariableDefinition}, -}; - -pub trait DocFormat { - fn doc(&self) -> String; -} - -impl DocFormat for DocComment { - fn doc(&self) -> String { - match self { - DocComment::Line { comment } => comment.value.to_owned(), - DocComment::Block { comments } => comments - .iter() - .map(|comment| comment.value.to_owned()) - .collect::>() - .join("\n"), - } - } -} - -impl DocFormat for Vec { - fn doc(&self) -> String { - self.iter().map(DocComment::doc).collect::>().join("\n") - } -} - -impl DocFormat for VariableDefinition { - fn doc(&self) -> String { - format!("### {}", self.name.name) - } -} - -impl DocFormat for FunctionDefinition { - fn doc(&self) -> String { - let name = self.name.as_ref().map_or(self.ty.to_string(), |n| n.name.to_owned()); - format!("### {}\n{}", name, self.ty) - } -} diff --git a/doc/src/format.rs b/doc/src/format.rs new file mode 100644 index 000000000000..66d7ba3141c8 --- /dev/null +++ b/doc/src/format.rs @@ -0,0 +1,65 @@ +use itertools::Itertools; +use solang_parser::{ + doccomment::DocComment, + pt::{Base, FunctionDefinition, VariableDefinition}, +}; + +use crate::output::DocOutput; + +pub trait DocFormat { + fn doc(&self) -> String; +} + +impl<'a> DocFormat for DocOutput<'a> { + fn doc(&self) -> String { + match self { + Self::H1(val) => format!("# {val}"), + Self::H2(val) => format!("## {val}"), + Self::H3(val) => format!("### {val}"), + Self::Bold(val) => format!("**{val}**"), + Self::Link(val, link) => format!("[{val}]({link})"), + } + } +} + +impl DocFormat for DocComment { + fn doc(&self) -> String { + match self { + DocComment::Line { comment } => comment.value.to_owned(), + DocComment::Block { comments } => { + comments.iter().map(|comment| comment.value.to_owned()).join("\n") + } + } + } +} + +impl DocFormat for Vec { + fn doc(&self) -> String { + self.iter().map(DocComment::doc).join("\n") + } +} + +impl DocFormat for Base { + fn doc(&self) -> String { + self.name.identifiers.iter().map(|ident| ident.name.clone()).join(".") + } +} + +impl DocFormat for Vec { + fn doc(&self) -> String { + self.iter().map(|base| base.doc()).join(", ") + } +} + +impl DocFormat for VariableDefinition { + fn doc(&self) -> String { + DocOutput::H3(&self.name.name).doc() + } +} + +impl DocFormat for FunctionDefinition { + fn doc(&self) -> String { + let name = self.name.as_ref().map_or(self.ty.to_string(), |n| n.name.to_owned()); + DocOutput::H3(&name).doc() + } +} diff --git a/doc/src/lib.rs b/doc/src/lib.rs index 88e52fc278ae..c900c35a5b17 100644 --- a/doc/src/lib.rs +++ b/doc/src/lib.rs @@ -9,8 +9,9 @@ use solang_parser::{ use thiserror::Error; pub mod builder; -mod doc_format; +mod format; mod macros; +mod output; #[derive(Error, Debug)] enum DocError {} // TODO: diff --git a/doc/src/output.rs b/doc/src/output.rs new file mode 100644 index 000000000000..bb1b5a053cdf --- /dev/null +++ b/doc/src/output.rs @@ -0,0 +1,15 @@ +use crate::format::DocFormat; + +pub enum DocOutput<'a> { + H1(&'a str), + H2(&'a str), + H3(&'a str), + Bold(&'a str), + Link(&'a str, &'a str), +} + +impl<'a> std::fmt::Display for DocOutput<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}", self.doc())) + } +} From 42576cf1c4497f704f935089fc8a0a2df44b5818 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Mon, 22 Aug 2022 08:51:54 +0300 Subject: [PATCH 09/67] inheritance cross linking --- Cargo.lock | 2 +- cli/src/cmd/forge/doc.rs | 11 ++-- doc/src/builder.rs | 105 +++++++++++++++++++++++++++++++-------- doc/src/format.rs | 7 +++ 4 files changed, 100 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 50cd7b9954ff..57dc67c997b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2010,7 +2010,7 @@ dependencies = [ name = "forge-doc" version = "0.1.0" dependencies = [ - "clap", + "clap 3.2.16", "ethers-solc", "eyre", "forge-fmt", diff --git a/cli/src/cmd/forge/doc.rs b/cli/src/cmd/forge/doc.rs index 6d11ee5eace7..3c01c8a83887 100644 --- a/cli/src/cmd/forge/doc.rs +++ b/cli/src/cmd/forge/doc.rs @@ -1,6 +1,6 @@ use crate::cmd::Cmd; use clap::{Parser, ValueHint}; -use forge_doc::builder::DocBuilder; +use forge_doc::builder::{DocBuilder, DocConfig}; use foundry_config::{find_project_root_path, load_config_with_root}; use std::path::PathBuf; @@ -21,8 +21,11 @@ impl Cmd for DocArgs { fn run(self) -> eyre::Result { let config = load_config_with_root(self.root.clone()); - DocBuilder::new(self.root.as_ref().unwrap_or(&find_project_root_path()?)) - .with_paths(config.project_paths().input_files()) - .build() + DocBuilder::from_config(DocConfig { + root: self.root.as_ref().unwrap_or(&find_project_root_path()?).to_path_buf(), + paths: config.project_paths().input_files(), + ..Default::default() + }) + .build() } } diff --git a/doc/src/builder.rs b/doc/src/builder.rs index fa69868ab3de..9a53a253d81d 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -1,38 +1,64 @@ use eyre; use forge_fmt::Visitable; use rayon::prelude::*; -use solang_parser::pt::{ContractTy, Identifier}; +use solang_parser::pt::{Base, ContractTy, Identifier}; use std::{ fmt::Write, fs, path::{Path, PathBuf}, }; -use crate::{format::DocFormat, macros::*, output::DocOutput, SolidityDoc, SolidityDocPartElement}; +use crate::{ + format::DocFormat, macros::*, output::DocOutput, SolidityDoc, SolidityDocPart, + SolidityDocPartElement, +}; pub struct DocBuilder { - root: PathBuf, - out_dir: PathBuf, - paths: Vec, + config: DocConfig, } -impl DocBuilder { +// TODO: move & merge w/ Figment +#[derive(Debug)] +pub struct DocConfig { + pub root: PathBuf, + pub paths: Vec, + pub out: PathBuf, + pub title: String, +} + +impl DocConfig { pub fn new(root: &Path) -> Self { - DocBuilder { root: root.to_owned(), out_dir: PathBuf::from("docs"), paths: vec![] } + DocConfig { root: root.to_owned(), ..Default::default() } } - pub fn with_paths(mut self, paths: Vec) -> Self { - self.paths = paths; - self + fn out_dir(&self) -> PathBuf { + self.root.join(&self.out) } +} - pub fn with_out_dir(mut self, out_dir: &Path) -> Self { - self.out_dir = out_dir.to_owned(); - self +impl Default for DocConfig { + fn default() -> Self { + DocConfig { + root: PathBuf::new(), + out: PathBuf::from("docs"), + title: "Title".to_owned(), + paths: vec![], + } + } +} + +impl DocBuilder { + pub fn new() -> Self { + DocBuilder { config: DocConfig::default() } + } + + pub fn from_config(config: DocConfig) -> Self { + DocBuilder { config } } pub fn build(self) -> eyre::Result<()> { let docs = self + .config .paths .par_iter() .enumerate() @@ -53,7 +79,7 @@ impl DocBuilder { }) .collect::>>()?; - let out_dir = self.root.join("docs"); // TODO: config + let out_dir = self.config.out_dir(); let out_dir_src = out_dir.join("src"); fs::create_dir_all(&out_dir_src)?; @@ -63,12 +89,21 @@ impl DocBuilder { if let SolidityDocPartElement::Contract(ref contract) = part.element { let mut doc_file = String::new(); writeln_doc!(doc_file, DocOutput::H1(&contract.name.name))?; + if !contract.base.is_empty() { + let bases = contract + .base + .iter() + .map(|base| { + Ok(self.lookup_contract_base(&docs, base)?.unwrap_or(base.doc())) + }) + .collect::>>()?; + writeln_doc!( doc_file, "{} {}\n", DocOutput::Bold("Inherits:"), - contract.base + bases.join(", ") )?; } @@ -92,19 +127,18 @@ impl DocBuilder { if !attributes.is_empty() { writeln_doc!(doc_file, DocOutput::H2("Attributes"))?; for (var, comments) in attributes { - writeln_doc!(doc_file, "{}{}\n", var, comments)?; + writeln_doc!(doc_file, "{}\n{}\n", var, comments)?; } } if !funcs.is_empty() { writeln_doc!(doc_file, DocOutput::H2("Functions"))?; for (func, comments) in funcs { - writeln_doc!(doc_file, "{}{}\n", func, comments)?; + writeln_doc!(doc_file, "{}\n{}\n", func, comments)?; } } - let mut new_path = path.strip_prefix(&self.root)?.to_owned(); - new_path.pop(); + let new_path = path.strip_prefix(&self.config.root)?.to_owned(); fs::create_dir_all(out_dir_src.join(&new_path))?; let new_path = new_path.join(&format!("contract.{}.md", contract.name)); @@ -155,7 +189,38 @@ impl DocBuilder { fs::write(out_dir_src.join("SUMMARY.md"), summary)?; // TODO: take values from config - fs::write(out_dir.join("book.toml"), "[book]\ntitle = \"Title\"\nsrc = \"src\"")?; + fs::write( + out_dir.join("book.toml"), + format!("[book]\ntitle = \"{}\"\nsrc = \"src\"", self.config.title), + )?; Ok(()) } + + fn lookup_contract_base( + &self, + docs: &Vec<(PathBuf, Vec)>, + base: &Base, + ) -> eyre::Result> { + for (base_path, base_doc) in docs.iter() { + for base_part in base_doc.iter() { + if let SolidityDocPartElement::Contract(base_contract) = &base_part.element { + if base.name.identifiers.last().unwrap().name == base_contract.name.name { + return Ok(Some(format!( + "[{}]({})", + base.doc(), + PathBuf::from("/") + .join( + base_path + .strip_prefix(&self.config.root)? + .join(&format!("contract.{}.md", base_contract.name)) + ) + .display(), + ))) + } + } + } + } + + Ok(None) + } } diff --git a/doc/src/format.rs b/doc/src/format.rs index 66d7ba3141c8..56d4c3291b2d 100644 --- a/doc/src/format.rs +++ b/doc/src/format.rs @@ -22,6 +22,13 @@ impl<'a> DocFormat for DocOutput<'a> { } } +// TODO: +impl DocFormat for String { + fn doc(&self) -> String { + self.to_owned() + } +} + impl DocFormat for DocComment { fn doc(&self) -> String { match self { From fe5ea039778e83f1caf9a18e32f2d893042d4982 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Wed, 21 Sep 2022 21:28:23 +0300 Subject: [PATCH 10/67] cont --- doc/Cargo.toml | 2 +- doc/src/as_code.rs | 178 +++++++++++++++++++++++++++++++++++++++++++++ doc/src/builder.rs | 153 ++++++++++++++++++++++++++++---------- doc/src/format.rs | 21 ++++-- doc/src/lib.rs | 44 +++++++---- doc/src/macros.rs | 10 +++ doc/src/output.rs | 1 + 7 files changed, 349 insertions(+), 60 deletions(-) create mode 100644 doc/src/as_code.rs diff --git a/doc/Cargo.toml b/doc/Cargo.toml index a040d550be9b..9af34e92d503 100644 --- a/doc/Cargo.toml +++ b/doc/Cargo.toml @@ -25,7 +25,7 @@ clap = { version = "3.0.10", features = [ ] } # misc -solang-parser = "0.1.17" +solang-parser = "=0.1.18" eyre = "0.6" thiserror = "1.0.30" rayon = "1.5.1" diff --git a/doc/src/as_code.rs b/doc/src/as_code.rs new file mode 100644 index 000000000000..196d6a2dbaf4 --- /dev/null +++ b/doc/src/as_code.rs @@ -0,0 +1,178 @@ +use forge_fmt::solang_ext::AttrSortKeyIteratorExt; +use itertools::Itertools; +use solang_parser::pt::{ + EventDefinition, EventParameter, Expression, FunctionAttribute, FunctionDefinition, + IdentifierPath, Loc, Parameter, Type, VariableAttribute, VariableDefinition, +}; + +pub trait AsCode { + fn as_code(&self) -> String; +} + +impl AsCode for VariableDefinition { + fn as_code(&self) -> String { + let ty = self.ty.as_code(); + let mut attrs = self.attrs.iter().attr_sorted().map(|attr| attr.as_code()).join(" "); + if !attrs.is_empty() { + attrs.insert(0, ' '); + } + let name = self.name.name.to_owned(); + format!("{ty}{attrs} {name}") + } +} + +impl AsCode for VariableAttribute { + fn as_code(&self) -> String { + match self { + VariableAttribute::Visibility(visibility) => visibility.to_string(), + VariableAttribute::Constant(_) => "constant".to_owned(), + VariableAttribute::Immutable(_) => "immutable".to_owned(), + VariableAttribute::Override(_, idents) => { + format!("override({})", idents.iter().map(AsCode::as_code).join(", ")) + } + } + } +} + +impl AsCode for FunctionDefinition { + fn as_code(&self) -> String { + let ty = self.ty.to_string(); + let name = self.name.as_ref().map(|n| format!(" {}", n.name)).unwrap_or_default(); + let params = self.params.as_code(); + let mut attributes = self.attributes.as_code(); + if !attributes.is_empty() { + attributes.insert(0, ' '); + } + let mut returns = self.returns.as_code(); + if !returns.is_empty() { + returns = format!(" returns ({})", returns) + } + format!("{ty}{name}({params}){attributes}{returns}") + } +} + +impl AsCode for Expression { + fn as_code(&self) -> String { + match self { + Expression::Type(_, ty) => { + match ty { + Type::Address => "address".to_owned(), + Type::AddressPayable => "address payable".to_owned(), + Type::Payable => "payable".to_owned(), + Type::Bool => "bool".to_owned(), + Type::String => "string".to_owned(), + Type::Bytes(n) => format!("bytes{}", n), + Type::Rational => "rational".to_owned(), + Type::DynamicBytes => "bytes".to_owned(), + Type::Int(n) => format!("int{}", n), + Type::Uint(n) => format!("uint{}", n), + Type::Mapping(_, from, to) => format!("mapping({} => {})", from.as_code(), to.as_code()), + Type::Function { params, attributes, returns } => { + let params = params.as_code(); + let mut attributes = attributes.as_code(); + if !attributes.is_empty() { + attributes.insert(0, ' '); + } + let mut returns_str = String::new(); + if let Some((params, _attrs)) = returns { + returns_str = params.as_code(); + if !returns_str.is_empty() { + returns_str = format!(" returns ({})", returns_str) + } + } + format!("function ({params}){attributes}{returns_str}") + }, + } + } + Expression::Variable(ident) => ident.name.to_owned(), + Expression::ArraySubscript(_, expr1, expr2) => format!("{}[{}]", expr1.as_code(), expr2.as_ref().map(|expr| expr.as_code()).unwrap_or_default()), + Expression::MemberAccess(_, expr, ident) => format!("{}.{}", ident.name, expr.as_code()), + item => { + println!("UNREACHABLE {:?}", item); // TODO: + unreachable!() + } + // ArraySlice( + // Loc, + // Box, + // Option>, + // Option>, + // ), + // FunctionCall(Loc, Box, Vec), + // NamedFunctionCall(Loc, Box, Vec), + // List(Loc, ParameterList), + } + } +} + +impl AsCode for FunctionAttribute { + fn as_code(&self) -> String { + match self { + Self::Mutability(mutability) => mutability.to_string(), + Self::Visibility(visibility) => visibility.to_string(), + Self::Virtual(_) => "virtual".to_owned(), + Self::Immutable(_) => "immutable".to_owned(), + Self::Override(_, idents) => { + format!("override({})", idents.iter().map(AsCode::as_code).join(", ")) + } + Self::BaseOrModifier(_, base) => "".to_owned(), // TODO: + Self::NameValue(..) => unreachable!(), + } + } +} + +impl AsCode for Parameter { + fn as_code(&self) -> String { + [ + Some(self.ty.as_code()), + self.storage.as_ref().map(|storage| storage.to_string()), + self.name.as_ref().map(|name| name.name.clone()), + ] + .into_iter() + .filter_map(|p| p) + .join(" ") + } +} + +impl AsCode for Vec<(Loc, Option)> { + fn as_code(&self) -> String { + self.iter() + .map(|(_, param)| param.as_ref().map(AsCode::as_code).unwrap_or_default()) + .join(", ") + } +} + +impl AsCode for Vec { + fn as_code(&self) -> String { + self.iter().attr_sorted().map(|attr| attr.as_code()).join(" ") + } +} + +impl AsCode for EventDefinition { + fn as_code(&self) -> String { + let name = &self.name.name; + let fields = self.fields.as_code(); + let anonymous = if self.anonymous { " anonymous" } else { "" }; + format!("event {name}({fields}){anonymous}") + } +} + +impl AsCode for EventParameter { + fn as_code(&self) -> String { + let ty = self.ty.as_code(); + let indexed = if self.indexed { " indexed" } else { "" }; + let name = self.name.as_ref().map(|name| name.name.to_owned()).unwrap_or_default(); + format!("{ty}{indexed} {name}") + } +} + +impl AsCode for Vec { + fn as_code(&self) -> String { + self.iter().map(AsCode::as_code).join(", ") + } +} + +impl AsCode for IdentifierPath { + fn as_code(&self) -> String { + self.identifiers.iter().map(|ident| ident.name.to_owned()).join(".") + } +} diff --git a/doc/src/builder.rs b/doc/src/builder.rs index 9a53a253d81d..6346e2cedf84 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -1,15 +1,20 @@ use eyre; use forge_fmt::Visitable; +use itertools::Itertools; use rayon::prelude::*; -use solang_parser::pt::{Base, ContractTy, Identifier}; +use solang_parser::{ + doccomment::DocComment, + pt::{Base, ContractTy, Identifier}, +}; use std::{ + collections::HashMap, fmt::Write, fs, path::{Path, PathBuf}, }; use crate::{ - format::DocFormat, macros::*, output::DocOutput, SolidityDoc, SolidityDocPart, + as_code::AsCode, format::DocFormat, macros::*, output::DocOutput, SolidityDoc, SolidityDocPart, SolidityDocPartElement, }; @@ -77,13 +82,14 @@ impl DocBuilder { Ok((path.clone(), doc.parts)) }) - .collect::>>()?; + .collect::>>()?; let out_dir = self.config.out_dir(); let out_dir_src = out_dir.join("src"); fs::create_dir_all(&out_dir_src)?; let mut filenames = vec![]; + for (path, doc) in docs.iter() { for part in doc.iter() { if let SolidityDocPartElement::Contract(ref contract) = part.element { @@ -111,8 +117,10 @@ impl DocBuilder { let mut attributes = vec![]; let mut funcs = vec![]; + let mut events = vec![]; for child in part.children.iter() { + // TODO: remove `clone`s match &child.element { SolidityDocPartElement::Function(func) => { funcs.push((func.clone(), child.comments.clone())) @@ -120,6 +128,9 @@ impl DocBuilder { SolidityDocPartElement::Variable(var) => { attributes.push((var.clone(), child.comments.clone())) } + SolidityDocPartElement::Event(event) => { + events.push((event.clone(), child.comments.clone())) + } _ => {} } } @@ -128,6 +139,8 @@ impl DocBuilder { writeln_doc!(doc_file, DocOutput::H2("Attributes"))?; for (var, comments) in attributes { writeln_doc!(doc_file, "{}\n{}\n", var, comments)?; + writeln_code!(doc_file, "{}", var)?; + writeln!(doc_file)?; } } @@ -135,6 +148,17 @@ impl DocBuilder { writeln_doc!(doc_file, DocOutput::H2("Functions"))?; for (func, comments) in funcs { writeln_doc!(doc_file, "{}\n{}\n", func, comments)?; + writeln_code!(doc_file, "{}", func)?; + writeln!(doc_file)?; + } + } + + if !events.is_empty() { + writeln_doc!(doc_file, DocOutput::H2("Events"))?; + for (ev, comments) in events { + writeln_doc!(doc_file, "{}\n{}\n", ev, comments)?; + writeln_code!(doc_file, "{}", ev)?; + writeln!(doc_file)?; } } @@ -151,54 +175,105 @@ impl DocBuilder { let mut summary = String::new(); writeln_doc!(summary, DocOutput::H1("Summary"))?; - // TODO: - let mut interfaces = vec![]; - let mut contracts = vec![]; - let mut abstract_contracts = vec![]; - let mut libraries = vec![]; - for entry in filenames.into_iter() { - match entry.1 { - ContractTy::Abstract(_) => abstract_contracts.push(entry), - ContractTy::Contract(_) => contracts.push(entry), - ContractTy::Interface(_) => interfaces.push(entry), - ContractTy::Library(_) => libraries.push(entry), + self.write_section(&mut summary, 0, None, &filenames)?; + + fs::write(out_dir_src.join("SUMMARY.md"), summary)?; + fs::write( + out_dir.join("book.toml"), + format!( + "[book]\ntitle = \"{}\"\nsrc = \"src\"\n\n[output.html]\nno-section-label = true\n\n[output.html.fold]\nenable = true", + self.config.title + ), + )?; + Ok(()) + } + + fn write_section( + &self, + out: &mut String, + depth: usize, + path: Option<&Path>, + entries: &[(Identifier, ContractTy, PathBuf)], + ) -> eyre::Result<()> { + if !entries.is_empty() { + if let Some(path) = path { + let title = path.iter().last().unwrap().to_string_lossy(); + let section_title = if depth == 1 { + DocOutput::H1(&title).doc() + } else { + format!( + "{}- {}", + " ".repeat((depth - 1) * 2), + DocOutput::Link( + &title, + &path.join("README.md").as_os_str().to_string_lossy() + ) + ) + }; + writeln_doc!(out, section_title)?; } - } - let mut write_section = |title: &str, - entries: &[(Identifier, ContractTy, PathBuf)]| - -> Result<(), std::fmt::Error> { - if !entries.is_empty() { - writeln_doc!(summary, DocOutput::H1(title))?; - for (src, _, filename) in entries { + let mut grouped = HashMap::new(); + for entry in entries { + let key = entry.2.iter().take(depth + 1).collect::(); + grouped.entry(key).or_insert_with(Vec::new).push(entry.clone()); + } + // TODO: + let grouped = grouped.iter().sorted_by(|(left_key, _), (right_key, _)| { + (left_key.extension().map(|ext| ext.eq("sol")).unwrap_or_default() as i32).cmp( + &(right_key.extension().map(|ext| ext.eq("sol")).unwrap_or_default() as i32), + ) + }); + + let indent = " ".repeat(2 * depth); + let mut readme = String::from("\n\n# Contents\n"); + for (path, entries) in grouped { + if path.extension().map(|ext| ext.eq("sol")).unwrap_or_default() { + for (src, _, filename) in entries { + writeln_doc!( + readme, + "- {}", + DocOutput::Link( + &src.name, + &Path::new("/").join(filename).display().to_string() + ) + )?; + + writeln_doc!( + out, + "{}- {}", + indent, + DocOutput::Link(&src.name, &filename.display().to_string()) + )?; + } + } else { writeln_doc!( - summary, + readme, "- {}", - DocOutput::Link(&src.name, &filename.display().to_string()) + DocOutput::Link( + &path.iter().last().unwrap().to_string_lossy(), + &path.join("README.md").display().to_string() + ) )?; + // let subsection = path.iter().last().unwrap().to_string_lossy(); + self.write_section(out, depth + 1, Some(&path), &entries)?; } - writeln!(summary)?; } - Ok(()) - }; - - write_section("Contracts", &contracts)?; - write_section("Libraries", &libraries)?; - write_section("Interfaces", &interfaces)?; - write_section("Abstract Contracts", &abstract_contracts)?; - - fs::write(out_dir_src.join("SUMMARY.md"), summary)?; - // TODO: take values from config - fs::write( - out_dir.join("book.toml"), - format!("[book]\ntitle = \"{}\"\nsrc = \"src\"", self.config.title), - )?; + if !readme.is_empty() { + if let Some(path) = path { + fs::write( + self.config.out_dir().join("src").join(path).join("README.md"), + readme, + )?; + } + } + } Ok(()) } fn lookup_contract_base( &self, - docs: &Vec<(PathBuf, Vec)>, + docs: &HashMap>, base: &Base, ) -> eyre::Result> { for (base_path, base_doc) in docs.iter() { diff --git a/doc/src/format.rs b/doc/src/format.rs index 56d4c3291b2d..27133763d4dc 100644 --- a/doc/src/format.rs +++ b/doc/src/format.rs @@ -1,11 +1,10 @@ +use crate::output::DocOutput; use itertools::Itertools; use solang_parser::{ doccomment::DocComment, - pt::{Base, FunctionDefinition, VariableDefinition}, + pt::{Base, EventDefinition, FunctionDefinition, VariableDefinition}, }; -use crate::output::DocOutput; - pub trait DocFormat { fn doc(&self) -> String; } @@ -18,11 +17,12 @@ impl<'a> DocFormat for DocOutput<'a> { Self::H3(val) => format!("### {val}"), Self::Bold(val) => format!("**{val}**"), Self::Link(val, link) => format!("[{val}]({link})"), + Self::CodeBlock(lang, val) => format!("```{lang}\n{val}\n```"), } } } -// TODO: +// TODO: change to return DocOutput impl DocFormat for String { fn doc(&self) -> String { self.to_owned() @@ -30,11 +30,12 @@ impl DocFormat for String { } impl DocFormat for DocComment { + // TODO: fn doc(&self) -> String { match self { DocComment::Line { comment } => comment.value.to_owned(), DocComment::Block { comments } => { - comments.iter().map(|comment| comment.value.to_owned()).join("\n") + comments.iter().map(|comment| comment.value.to_owned()).join("\n\n") } } } @@ -42,13 +43,13 @@ impl DocFormat for DocComment { impl DocFormat for Vec { fn doc(&self) -> String { - self.iter().map(DocComment::doc).join("\n") + self.iter().map(DocComment::doc).join("\n\n") } } impl DocFormat for Base { fn doc(&self) -> String { - self.name.identifiers.iter().map(|ident| ident.name.clone()).join(".") + self.name.identifiers.iter().map(|ident| ident.name.to_owned()).join(".") } } @@ -70,3 +71,9 @@ impl DocFormat for FunctionDefinition { DocOutput::H3(&name).doc() } } + +impl DocFormat for EventDefinition { + fn doc(&self) -> String { + DocOutput::H3(&self.name.name).doc() + } +} diff --git a/doc/src/lib.rs b/doc/src/lib.rs index c900c35a5b17..da210bc71e30 100644 --- a/doc/src/lib.rs +++ b/doc/src/lib.rs @@ -2,12 +2,13 @@ use forge_fmt::{Visitable, Visitor}; use solang_parser::{ doccomment::{parse_doccomments, DocComment}, pt::{ - Comment, ContractDefinition, FunctionDefinition, Loc, SourceUnit, SourceUnitPart, - VariableDefinition, + Comment, ContractDefinition, EventDefinition, FunctionDefinition, Loc, SourceUnit, + SourceUnitPart, VariableDefinition, }, }; use thiserror::Error; +mod as_code; pub mod builder; mod format; mod macros; @@ -23,7 +24,18 @@ struct SolidityDoc { parts: Vec, comments: Vec, start_at: usize, - curr_parent: Option, + context: DocContext, +} + +#[derive(Debug)] +struct DocContext { + parent: Option, +} + +impl Default for DocContext { + fn default() -> Self { + Self { parent: None } + } } #[derive(Debug, PartialEq)] @@ -35,7 +47,7 @@ struct SolidityDocPart { impl SolidityDoc { fn new(comments: Vec) -> Self { - SolidityDoc { parts: vec![], comments, start_at: 0, curr_parent: None } + SolidityDoc { parts: vec![], comments, start_at: 0, context: Default::default() } } fn with_parent( @@ -43,18 +55,18 @@ impl SolidityDoc { mut parent: SolidityDocPart, mut visit: impl FnMut(&mut Self) -> Result<()>, ) -> Result { - let curr = self.curr_parent.take(); - self.curr_parent = Some(parent); + let curr = self.context.parent.take(); + self.context.parent = Some(parent); visit(self)?; - parent = self.curr_parent.take().unwrap(); - self.curr_parent = curr; + parent = self.context.parent.take().unwrap(); + self.context.parent = curr; Ok(parent) } fn add_element_to_parent(&mut self, element: SolidityDocPartElement, loc: Loc) { let child = SolidityDocPart { comments: self.parse_docs(loc.start()), element, children: vec![] }; - if let Some(parent) = self.curr_parent.as_mut() { + if let Some(parent) = self.context.parent.as_mut() { parent.children.push(child); } else { self.parts.push(child); @@ -72,6 +84,7 @@ enum SolidityDocPartElement { Contract(Box), Function(FunctionDefinition), Variable(VariableDefinition), + Event(EventDefinition), } impl Visitor for SolidityDoc { @@ -88,10 +101,7 @@ impl Visitor for SolidityDoc { }; self.start_at = def.loc.start(); let contract = self.with_parent(contract, |doc| { - for d in def.parts.iter_mut() { - d.visit(doc)?; - } - + def.parts.iter_mut().map(|d| d.visit(doc)).collect::>>()?; Ok(()) })?; @@ -99,6 +109,7 @@ impl Visitor for SolidityDoc { self.parts.push(contract); } SourceUnitPart::FunctionDefinition(func) => self.visit_function(func)?, + SourceUnitPart::EventDefinition(event) => self.visit_event(event)?, _ => {} }; } @@ -115,6 +126,13 @@ impl Visitor for SolidityDoc { self.add_element_to_parent(SolidityDocPartElement::Variable(var.clone()), var.loc); Ok(()) } + + fn visit_event(&mut self, event: &mut EventDefinition) -> Result<()> { + self.add_element_to_parent(SolidityDocPartElement::Event(event.clone()), event.loc); + Ok(()) + } + + // TODO: structs } #[cfg(test)] diff --git a/doc/src/macros.rs b/doc/src/macros.rs index 4617afa0c65e..36485c67081a 100644 --- a/doc/src/macros.rs +++ b/doc/src/macros.rs @@ -7,4 +7,14 @@ macro_rules! writeln_doc { }; } +macro_rules! writeln_code { + ($dst:expr, $arg:expr) => { + writeln_code!($dst, "{}", $arg) + }; + ($dst:expr, $format:literal, $($arg:expr),*) => { + writeln!($dst, "{}", $crate::output::DocOutput::CodeBlock("solidity", &format!($format, $($arg.as_code(),)*))) + }; +} + +pub(crate) use writeln_code; pub(crate) use writeln_doc; diff --git a/doc/src/output.rs b/doc/src/output.rs index bb1b5a053cdf..0fbb239589eb 100644 --- a/doc/src/output.rs +++ b/doc/src/output.rs @@ -6,6 +6,7 @@ pub enum DocOutput<'a> { H3(&'a str), Bold(&'a str), Link(&'a str, &'a str), + CodeBlock(&'a str, &'a str), } impl<'a> std::fmt::Display for DocOutput<'a> { From 1cbad5b39a32407ffd2397b73040843311376950 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Fri, 23 Sep 2022 14:01:38 +0300 Subject: [PATCH 11/67] cont --- Cargo.lock | 2 +- cli/src/cmd/forge/doc.rs | 2 +- doc/src/builder.rs | 221 ++++++++++++++++++++++++++++--------- doc/src/format.rs | 22 ++-- doc/src/helpers.rs | 16 +++ doc/src/lib.rs | 17 ++- doc/static/solidity.min.js | 74 +++++++++++++ 7 files changed, 287 insertions(+), 67 deletions(-) create mode 100644 doc/src/helpers.rs create mode 100644 doc/static/solidity.min.js diff --git a/Cargo.lock b/Cargo.lock index 9997a37ab1a1..d0383e64bdec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2077,7 +2077,7 @@ dependencies = [ name = "forge-doc" version = "0.1.0" dependencies = [ - "clap 3.2.16", + "clap", "ethers-solc", "eyre", "forge-fmt", diff --git a/cli/src/cmd/forge/doc.rs b/cli/src/cmd/forge/doc.rs index 3c01c8a83887..4a409e62d494 100644 --- a/cli/src/cmd/forge/doc.rs +++ b/cli/src/cmd/forge/doc.rs @@ -23,7 +23,7 @@ impl Cmd for DocArgs { let config = load_config_with_root(self.root.clone()); DocBuilder::from_config(DocConfig { root: self.root.as_ref().unwrap_or(&find_project_root_path()?).to_path_buf(), - paths: config.project_paths().input_files(), + sources: config.project_paths().sources, ..Default::default() }) .build() diff --git a/doc/src/builder.rs b/doc/src/builder.rs index 6346e2cedf84..303300a7d0be 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -3,10 +3,11 @@ use forge_fmt::Visitable; use itertools::Itertools; use rayon::prelude::*; use solang_parser::{ - doccomment::DocComment, - pt::{Base, ContractTy, Identifier}, + doccomment::{DocComment, DocCommentTag}, + pt::{Base, ContractTy, Identifier, Parameter}, }; use std::{ + cmp::Ordering, collections::HashMap, fmt::Write, fs, @@ -14,8 +15,8 @@ use std::{ }; use crate::{ - as_code::AsCode, format::DocFormat, macros::*, output::DocOutput, SolidityDoc, SolidityDocPart, - SolidityDocPartElement, + as_code::AsCode, format::DocFormat, helpers::*, macros::*, output::DocOutput, SolidityDoc, + SolidityDocPart, SolidityDocPartElement, }; pub struct DocBuilder { @@ -26,7 +27,7 @@ pub struct DocBuilder { #[derive(Debug)] pub struct DocConfig { pub root: PathBuf, - pub paths: Vec, + pub sources: PathBuf, pub out: PathBuf, pub title: String, } @@ -45,9 +46,9 @@ impl Default for DocConfig { fn default() -> Self { DocConfig { root: PathBuf::new(), + sources: PathBuf::new(), out: PathBuf::from("docs"), - title: "Title".to_owned(), - paths: vec![], + title: "".to_owned(), } } } @@ -61,10 +62,13 @@ impl DocBuilder { DocBuilder { config } } + pub fn out_dir_src(&self) -> PathBuf { + self.config.out_dir().join("src") + } + pub fn build(self) -> eyre::Result<()> { - let docs = self - .config - .paths + let sources: Vec<_> = ethers_solc::utils::source_files_iter(&self.config.sources).collect(); + let docs = sources .par_iter() .enumerate() .map(|(i, path)| { @@ -82,15 +86,16 @@ impl DocBuilder { Ok((path.clone(), doc.parts)) }) - .collect::>>()?; + .collect::>>()?; - let out_dir = self.config.out_dir(); - let out_dir_src = out_dir.join("src"); - fs::create_dir_all(&out_dir_src)?; + let docs = docs.into_iter().sorted_by(|(path1, _), (path2, _)| { + path1.display().to_string().cmp(&path2.display().to_string()) + }); - let mut filenames = vec![]; + fs::create_dir_all(self.out_dir_src())?; - for (path, doc) in docs.iter() { + let mut filenames = vec![]; + for (path, doc) in docs.as_ref() { for part in doc.iter() { if let SolidityDocPartElement::Contract(ref contract) = part.element { let mut doc_file = String::new(); @@ -101,7 +106,9 @@ impl DocBuilder { .base .iter() .map(|base| { - Ok(self.lookup_contract_base(&docs, base)?.unwrap_or(base.doc())) + Ok(self + .lookup_contract_base(docs.as_ref(), base)? + .unwrap_or(base.doc())) }) .collect::>>()?; @@ -123,13 +130,13 @@ impl DocBuilder { // TODO: remove `clone`s match &child.element { SolidityDocPartElement::Function(func) => { - funcs.push((func.clone(), child.comments.clone())) + funcs.push((func, &child.comments)) } SolidityDocPartElement::Variable(var) => { - attributes.push((var.clone(), child.comments.clone())) + attributes.push((var, &child.comments)) } SolidityDocPartElement::Event(event) => { - events.push((event.clone(), child.comments.clone())) + events.push((event, &child.comments)) } _ => {} } @@ -147,8 +154,46 @@ impl DocBuilder { if !funcs.is_empty() { writeln_doc!(doc_file, DocOutput::H2("Functions"))?; for (func, comments) in funcs { - writeln_doc!(doc_file, "{}\n{}\n", func, comments)?; + writeln_doc!(doc_file, "{}\n", func)?; + writeln_doc!( + doc_file, + "{}\n", + filter_comments_without_tags(&comments, vec!["param", "return"]) + )?; writeln_code!(doc_file, "{}", func)?; + + let params: Vec<_> = + func.params.iter().filter_map(|p| p.1.as_ref()).collect(); + let param_comments = filter_comments_by_tag(&comments, "param"); + if !params.is_empty() && !param_comments.is_empty() { + writeln_doc!( + doc_file, + "{}\n{}", + DocOutput::H3("Parameters"), + self.format_comment_table( + &["Name", "Type", "Description"], + ¶ms, + ¶m_comments + )? + )?; + } + + let returns: Vec<_> = + func.returns.iter().filter_map(|p| p.1.as_ref()).collect(); + let returns_comments = filter_comments_by_tag(&comments, "return"); + if !returns.is_empty() && !returns_comments.is_empty() { + writeln_doc!( + doc_file, + "{}\n{}", + DocOutput::H3("Returns"), + self.format_comment_table( + &["Name", "Type", "Description"], + ¶ms, + &returns_comments + )? + )?; + } + writeln!(doc_file)?; } } @@ -163,32 +208,106 @@ impl DocBuilder { } let new_path = path.strip_prefix(&self.config.root)?.to_owned(); - fs::create_dir_all(out_dir_src.join(&new_path))?; + fs::create_dir_all(self.out_dir_src().join(&new_path))?; let new_path = new_path.join(&format!("contract.{}.md", contract.name)); - fs::write(out_dir_src.join(&new_path), doc_file)?; + fs::write(self.out_dir_src().join(&new_path), doc_file)?; filenames.push((contract.name.clone(), contract.ty.clone(), new_path)); } } } + self.write_book_config(filenames)?; + + Ok(()) + } + + fn format_comment_table( + &self, + headers: &[&str], + params: &[&Parameter], + comments: &[&DocCommentTag], + ) -> eyre::Result { + let mut out = String::new(); + let separator = headers.iter().map(|h| "-".repeat(h.len())).join("|"); + + writeln!(out, "|{}|", headers.join("|"))?; + writeln!(out, "|{separator}|")?; + for param in params { + let param_name = param.name.as_ref().map(|n| n.name.to_owned()); + let description = param_name + .as_ref() + .map(|name| { + comments.iter().find_map(|comment| { + match comment.value.trim_start().split_once(' ') { + Some((tag_name, description)) if tag_name.trim().eq(name.as_str()) => { + Some(description.replace("\n", " ")) + } + _ => None, + } + }) + }) + .flatten() + .unwrap_or_default(); + let row = [ + param_name.unwrap_or_else(|| "".to_owned()), + param.ty.as_code(), + description, + ]; + writeln!(out, "|{}|", row.join("|"))?; + } + + Ok(out) + } + + fn write_book_config( + &self, + filenames: Vec<(Identifier, ContractTy, PathBuf)>, + ) -> eyre::Result<()> { + let out_dir_src = self.out_dir_src(); + + let readme_content = { + let src_readme = self.config.sources.join("README.md"); + if src_readme.exists() { + fs::read_to_string(src_readme)? + } else { + String::new() + } + }; + let readme_path = out_dir_src.join("README.md"); + fs::write(&readme_path, readme_content)?; + let mut summary = String::new(); writeln_doc!(summary, DocOutput::H1("Summary"))?; + writeln_doc!( + summary, + "- {}", + DocOutput::Link("README", &readme_path.display().to_string()) + )?; + + self.write_summary_section(&mut summary, 0, None, &filenames)?; - self.write_section(&mut summary, 0, None, &filenames)?; + fs::write(out_dir_src.join("SUMMARY.md"), summary.clone())?; + + let out_dir_static = out_dir_src.join("static"); + fs::create_dir_all(&out_dir_static)?; + fs::write( + out_dir_static.join("solidity.min.js"), + include_str!("../static/solidity.min.js"), + )?; - fs::write(out_dir_src.join("SUMMARY.md"), summary)?; fs::write( - out_dir.join("book.toml"), + self.config.out_dir().join("book.toml"), format!( - "[book]\ntitle = \"{}\"\nsrc = \"src\"\n\n[output.html]\nno-section-label = true\n\n[output.html.fold]\nenable = true", + "[book]\ntitle = \"{}\"\nsrc = \"src\"\n\n[output.html]\nno-section-label = true\nadditional-js = [\"src/static/solidity.min.js\"]\n\n[output.html.fold]\nenable = true\nlevel = 2", self.config.title ), )?; + Ok(()) } - fn write_section( + fn write_summary_section( &self, out: &mut String, depth: usize, @@ -213,16 +332,22 @@ impl DocBuilder { writeln_doc!(out, section_title)?; } + // group and sort entries let mut grouped = HashMap::new(); for entry in entries { let key = entry.2.iter().take(depth + 1).collect::(); grouped.entry(key).or_insert_with(Vec::new).push(entry.clone()); } - // TODO: - let grouped = grouped.iter().sorted_by(|(left_key, _), (right_key, _)| { - (left_key.extension().map(|ext| ext.eq("sol")).unwrap_or_default() as i32).cmp( - &(right_key.extension().map(|ext| ext.eq("sol")).unwrap_or_default() as i32), - ) + let grouped = grouped.iter().sorted_by(|(lhs, _), (rhs, _)| { + let lhs_at_end = lhs.extension().map(|ext| ext.eq("sol")).unwrap_or_default(); + let rhs_at_end = rhs.extension().map(|ext| ext.eq("sol")).unwrap_or_default(); + if lhs_at_end == rhs_at_end { + lhs.cmp(rhs) + } else if lhs_at_end { + Ordering::Greater + } else { + Ordering::Less + } }); let indent = " ".repeat(2 * depth); @@ -252,11 +377,10 @@ impl DocBuilder { "- {}", DocOutput::Link( &path.iter().last().unwrap().to_string_lossy(), - &path.join("README.md").display().to_string() + &Path::new("/").join(path).display().to_string() ) )?; - // let subsection = path.iter().last().unwrap().to_string_lossy(); - self.write_section(out, depth + 1, Some(&path), &entries)?; + self.write_summary_section(out, depth + 1, Some(&path), &entries)?; } } if !readme.is_empty() { @@ -271,26 +395,23 @@ impl DocBuilder { Ok(()) } - fn lookup_contract_base( + fn lookup_contract_base<'a>( &self, - docs: &HashMap>, + docs: &[(PathBuf, Vec)], base: &Base, ) -> eyre::Result> { - for (base_path, base_doc) in docs.iter() { + for (base_path, base_doc) in docs { for base_part in base_doc.iter() { if let SolidityDocPartElement::Contract(base_contract) = &base_part.element { if base.name.identifiers.last().unwrap().name == base_contract.name.name { - return Ok(Some(format!( - "[{}]({})", - base.doc(), - PathBuf::from("/") - .join( - base_path - .strip_prefix(&self.config.root)? - .join(&format!("contract.{}.md", base_contract.name)) - ) - .display(), - ))) + let path = PathBuf::from("/").join( + base_path + .strip_prefix(&self.config.root)? + .join(&format!("contract.{}.md", base_contract.name)), + ); + return Ok(Some( + DocOutput::Link(&base.doc(), &path.display().to_string()).doc(), + )) } } } diff --git a/doc/src/format.rs b/doc/src/format.rs index 27133763d4dc..f4a62a5dbb2e 100644 --- a/doc/src/format.rs +++ b/doc/src/format.rs @@ -1,7 +1,7 @@ use crate::output::DocOutput; use itertools::Itertools; use solang_parser::{ - doccomment::DocComment, + doccomment::DocCommentTag, pt::{Base, EventDefinition, FunctionDefinition, VariableDefinition}, }; @@ -29,21 +29,21 @@ impl DocFormat for String { } } -impl DocFormat for DocComment { - // TODO: +impl DocFormat for DocCommentTag { fn doc(&self) -> String { - match self { - DocComment::Line { comment } => comment.value.to_owned(), - DocComment::Block { comments } => { - comments.iter().map(|comment| comment.value.to_owned()).join("\n\n") - } - } + self.value.to_owned() + } +} + +impl DocFormat for Vec<&DocCommentTag> { + fn doc(&self) -> String { + self.iter().map(|c| DocCommentTag::doc(*c)).join("\n\n") } } -impl DocFormat for Vec { +impl DocFormat for Vec { fn doc(&self) -> String { - self.iter().map(DocComment::doc).join("\n\n") + self.iter().map(DocCommentTag::doc).join("\n\n") } } diff --git a/doc/src/helpers.rs b/doc/src/helpers.rs new file mode 100644 index 000000000000..72b28755b240 --- /dev/null +++ b/doc/src/helpers.rs @@ -0,0 +1,16 @@ +use solang_parser::doccomment::DocCommentTag; + +/// TODO: +pub fn filter_comments_by_tag<'a>( + comments: &'a Vec, + tag: &str, +) -> Vec<&'a DocCommentTag> { + comments.iter().filter(|c| c.tag == tag).collect() +} + +pub fn filter_comments_without_tags<'a>( + comments: &'a Vec, + tags: Vec<&str>, +) -> Vec<&'a DocCommentTag> { + comments.iter().filter(|c| !tags.contains(&c.tag.as_str())).collect() +} diff --git a/doc/src/lib.rs b/doc/src/lib.rs index da210bc71e30..8a646f7578e4 100644 --- a/doc/src/lib.rs +++ b/doc/src/lib.rs @@ -1,6 +1,7 @@ use forge_fmt::{Visitable, Visitor}; +use itertools::Itertools; use solang_parser::{ - doccomment::{parse_doccomments, DocComment}, + doccomment::{parse_doccomments, DocComment, DocCommentTag}, pt::{ Comment, ContractDefinition, EventDefinition, FunctionDefinition, Loc, SourceUnit, SourceUnitPart, VariableDefinition, @@ -11,6 +12,7 @@ use thiserror::Error; mod as_code; pub mod builder; mod format; +mod helpers; mod macros; mod output; @@ -40,7 +42,7 @@ impl Default for DocContext { #[derive(Debug, PartialEq)] struct SolidityDocPart { - comments: Vec, + comments: Vec, element: SolidityDocPartElement, children: Vec, } @@ -74,8 +76,15 @@ impl SolidityDoc { self.start_at = loc.end(); } - fn parse_docs(&mut self, end: usize) -> Vec { - parse_doccomments(&self.comments, self.start_at, end) + fn parse_docs(&mut self, end: usize) -> Vec { + let mut res = vec![]; + for comment in parse_doccomments(&self.comments, self.start_at, end) { + match comment { + DocComment::Line { comment } => res.push(comment), + DocComment::Block { comments } => res.extend(comments.into_iter()), + } + } + res } } diff --git a/doc/static/solidity.min.js b/doc/static/solidity.min.js new file mode 100644 index 000000000000..1924932919cf --- /dev/null +++ b/doc/static/solidity.min.js @@ -0,0 +1,74 @@ +hljs.registerLanguage("solidity",(()=>{"use strict";function e(){try{return!0 +}catch(e){return!1}} +var a=/-?(\b0[xX]([a-fA-F0-9]_?)*[a-fA-F0-9]|(\b[1-9](_?\d)*(\.((\d_?)*\d)?)?|\.\d(_?\d)*)([eE][-+]?\d(_?\d)*)?|\b0)(?!\w|\$)/ +;e()&&(a=a.source.replace(/\\b/g,"(?{ +var a=r(e),o=l(e),c=/[A-Za-z_$][A-Za-z_$0-9.]*/,d=e.inherit(e.TITLE_MODE,{ +begin:/[A-Za-z$_][0-9A-Za-z$_]*/,lexemes:c,keywords:n}),u={className:"params", +begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,lexemes:c,keywords:n, +contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,o,s]},_={ +className:"operator",begin:/:=|->/};return{keywords:n,lexemes:c, +contains:[a,o,i,t,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,s,_,{ +className:"function",lexemes:c,beginKeywords:"function",end:"{",excludeEnd:!0, +contains:[d,u,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,_]}]}}, +solAposStringMode:r,solQuoteStringMode:l,HEX_APOS_STRING_MODE:i, +HEX_QUOTE_STRING_MODE:t,SOL_NUMBER:s,isNegativeLookbehindAvailable:e} +;const{baseAssembly:c,solAposStringMode:d,solQuoteStringMode:u,HEX_APOS_STRING_MODE:_,HEX_QUOTE_STRING_MODE:m,SOL_NUMBER:b,isNegativeLookbehindAvailable:E}=o +;return e=>{for(var a=d(e),s=u(e),n=[],i=0;i<32;i++)n[i]=i+1 +;var t=n.map((e=>8*e)),r=[];for(i=0;i<=80;i++)r[i]=i +;var l=n.map((e=>"bytes"+e)).join(" ")+" ",o=t.map((e=>"uint"+e)).join(" ")+" ",g=t.map((e=>"int"+e)).join(" ")+" ",M=[].concat.apply([],t.map((e=>r.map((a=>e+"x"+a))))),p={ +keyword:"var bool string int uint "+g+o+"byte bytes "+l+"fixed ufixed "+M.map((e=>"fixed"+e)).join(" ")+" "+M.map((e=>"ufixed"+e)).join(" ")+" enum struct mapping address new delete if else for while continue break return throw emit try catch revert unchecked _ function modifier event constructor fallback receive error virtual override constant immutable anonymous indexed storage memory calldata external public internal payable pure view private returns import from as using pragma contract interface library is abstract type assembly", +literal:"true false wei gwei szabo finney ether seconds minutes hours days weeks years", +built_in:"self this super selfdestruct suicide now msg block tx abi blockhash gasleft assert require Error Panic sha3 sha256 keccak256 ripemd160 ecrecover addmod mulmod log0 log1 log2 log3 log4" +},O={className:"operator",begin:/[+\-!~*\/%<>&^|=]/ +},C=/[A-Za-z_$][A-Za-z_$0-9]*/,N={className:"params",begin:/\(/,end:/\)/, +excludeBegin:!0,excludeEnd:!0,lexemes:C,keywords:p, +contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,s,b,"self"]},f={ +begin:/\.\s*/,end:/[^A-Za-z0-9$_\.]/,excludeBegin:!0,excludeEnd:!0,keywords:{ +built_in:"gas value selector address length push pop send transfer call callcode delegatecall staticcall balance code codehash wrap unwrap name creationCode runtimeCode interfaceId min max" +},relevance:2},y=e.inherit(e.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/, +lexemes:C,keywords:p}),w={className:"built_in", +begin:(E()?"(? Date: Fri, 23 Sep 2022 14:02:38 +0300 Subject: [PATCH 12/67] rm default level --- doc/src/builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/builder.rs b/doc/src/builder.rs index 303300a7d0be..a4813f0e4d8d 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -299,7 +299,7 @@ impl DocBuilder { fs::write( self.config.out_dir().join("book.toml"), format!( - "[book]\ntitle = \"{}\"\nsrc = \"src\"\n\n[output.html]\nno-section-label = true\nadditional-js = [\"src/static/solidity.min.js\"]\n\n[output.html.fold]\nenable = true\nlevel = 2", + "[book]\ntitle = \"{}\"\nsrc = \"src\"\n\n[output.html]\nno-section-label = true\nadditional-js = [\"src/static/solidity.min.js\"]\n\n[output.html.fold]\nenable = true", self.config.title ), )?; From 46b424efe73d30c71bf7aa65dfa852ad6fe311df Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Fri, 23 Sep 2022 15:51:23 +0300 Subject: [PATCH 13/67] book.toml & readme entry --- Cargo.lock | 1 + doc/Cargo.toml | 1 + doc/src/builder.rs | 16 ++++++++-------- doc/static/book.toml | 9 +++++++++ 4 files changed, 19 insertions(+), 8 deletions(-) create mode 100644 doc/static/book.toml diff --git a/Cargo.lock b/Cargo.lock index d0383e64bdec..fe4d551b7376 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2086,6 +2086,7 @@ dependencies = [ "rayon", "solang-parser", "thiserror", + "toml", ] [[package]] diff --git a/doc/Cargo.toml b/doc/Cargo.toml index 9af34e92d503..39e951d39bc5 100644 --- a/doc/Cargo.toml +++ b/doc/Cargo.toml @@ -30,3 +30,4 @@ eyre = "0.6" thiserror = "1.0.30" rayon = "1.5.1" itertools = "0.10.3" +toml = "0.5" \ No newline at end of file diff --git a/doc/src/builder.rs b/doc/src/builder.rs index a4813f0e4d8d..104f06d66781 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -282,7 +282,10 @@ impl DocBuilder { writeln_doc!( summary, "- {}", - DocOutput::Link("README", &readme_path.display().to_string()) + DocOutput::Link( + "README", + &readme_path.strip_prefix(&self.config.root)?.display().to_string() + ) )?; self.write_summary_section(&mut summary, 0, None, &filenames)?; @@ -296,13 +299,10 @@ impl DocBuilder { include_str!("../static/solidity.min.js"), )?; - fs::write( - self.config.out_dir().join("book.toml"), - format!( - "[book]\ntitle = \"{}\"\nsrc = \"src\"\n\n[output.html]\nno-section-label = true\nadditional-js = [\"src/static/solidity.min.js\"]\n\n[output.html.fold]\nenable = true", - self.config.title - ), - )?; + let mut book: toml::Value = toml::from_str(include_str!("../static/book.toml"))?; + let book_entry = book["book"].as_table_mut().unwrap(); + book_entry.insert(String::from("title"), self.config.title.clone().into()); + fs::write(self.config.out_dir().join("book.toml"), toml::to_string_pretty(&book)?)?; Ok(()) } diff --git a/doc/static/book.toml b/doc/static/book.toml new file mode 100644 index 000000000000..d2947a7acef3 --- /dev/null +++ b/doc/static/book.toml @@ -0,0 +1,9 @@ +[book] +src = "src" + +[output.html] +no-section-label = true +additional-js = ["src/static/solidity.min.js"] + +[output.html.fold] +enable = true From 3b436f09cebb3fa3664b18f2bddc682796f82caa Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Fri, 23 Sep 2022 16:03:53 +0300 Subject: [PATCH 14/67] fix readme entry --- doc/src/builder.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/doc/src/builder.rs b/doc/src/builder.rs index 104f06d66781..631c8ecb2133 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -279,14 +279,7 @@ impl DocBuilder { let mut summary = String::new(); writeln_doc!(summary, DocOutput::H1("Summary"))?; - writeln_doc!( - summary, - "- {}", - DocOutput::Link( - "README", - &readme_path.strip_prefix(&self.config.root)?.display().to_string() - ) - )?; + writeln_doc!(summary, "- {}", DocOutput::Link("README", "README.md"))?; self.write_summary_section(&mut summary, 0, None, &filenames)?; From 1fe454626e46166c9ed26ab49a69b457ec705d38 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Thu, 27 Oct 2022 14:53:38 +0300 Subject: [PATCH 15/67] clippy --- Cargo.lock | 8 +++++++- doc/src/as_code.rs | 4 ++-- doc/src/builder.rs | 2 +- doc/src/format.rs | 1 - doc/src/helpers.rs | 4 ++-- doc/src/lib.rs | 9 +-------- 6 files changed, 13 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a9bd016acc55..be904741b4be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -790,7 +790,9 @@ dependencies = [ "once_cell", "strsim", "termcolor", + "terminal_size 0.1.17", "textwrap", + "unicase", ] [[package]] @@ -2127,7 +2129,7 @@ dependencies = [ name = "forge-doc" version = "0.1.0" dependencies = [ - "clap", + "clap 3.2.16", "ethers-solc", "eyre", "forge-fmt", @@ -5561,6 +5563,10 @@ name = "textwrap" version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" +dependencies = [ + "terminal_size 0.1.17", + "unicode-width", +] [[package]] name = "thiserror" diff --git a/doc/src/as_code.rs b/doc/src/as_code.rs index 196d6a2dbaf4..87e7102d7a3d 100644 --- a/doc/src/as_code.rs +++ b/doc/src/as_code.rs @@ -114,7 +114,7 @@ impl AsCode for FunctionAttribute { Self::Override(_, idents) => { format!("override({})", idents.iter().map(AsCode::as_code).join(", ")) } - Self::BaseOrModifier(_, base) => "".to_owned(), // TODO: + Self::BaseOrModifier(_, _base) => "".to_owned(), // TODO: Self::NameValue(..) => unreachable!(), } } @@ -128,7 +128,7 @@ impl AsCode for Parameter { self.name.as_ref().map(|name| name.name.clone()), ] .into_iter() - .filter_map(|p| p) + .flatten() .join(" ") } } diff --git a/doc/src/builder.rs b/doc/src/builder.rs index 631c8ecb2133..0689efea78ac 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -3,7 +3,7 @@ use forge_fmt::Visitable; use itertools::Itertools; use rayon::prelude::*; use solang_parser::{ - doccomment::{DocComment, DocCommentTag}, + doccomment::DocCommentTag, pt::{Base, ContractTy, Identifier, Parameter}, }; use std::{ diff --git a/doc/src/format.rs b/doc/src/format.rs index f4a62a5dbb2e..8291a2482f91 100644 --- a/doc/src/format.rs +++ b/doc/src/format.rs @@ -22,7 +22,6 @@ impl<'a> DocFormat for DocOutput<'a> { } } -// TODO: change to return DocOutput impl DocFormat for String { fn doc(&self) -> String { self.to_owned() diff --git a/doc/src/helpers.rs b/doc/src/helpers.rs index 72b28755b240..976937e4dae2 100644 --- a/doc/src/helpers.rs +++ b/doc/src/helpers.rs @@ -2,14 +2,14 @@ use solang_parser::doccomment::DocCommentTag; /// TODO: pub fn filter_comments_by_tag<'a>( - comments: &'a Vec, + comments: &'a [DocCommentTag], tag: &str, ) -> Vec<&'a DocCommentTag> { comments.iter().filter(|c| c.tag == tag).collect() } pub fn filter_comments_without_tags<'a>( - comments: &'a Vec, + comments: &'a [DocCommentTag], tags: Vec<&str>, ) -> Vec<&'a DocCommentTag> { comments.iter().filter(|c| !tags.contains(&c.tag.as_str())).collect() diff --git a/doc/src/lib.rs b/doc/src/lib.rs index 8a646f7578e4..3dbb0252c494 100644 --- a/doc/src/lib.rs +++ b/doc/src/lib.rs @@ -1,5 +1,4 @@ use forge_fmt::{Visitable, Visitor}; -use itertools::Itertools; use solang_parser::{ doccomment::{parse_doccomments, DocComment, DocCommentTag}, pt::{ @@ -29,17 +28,11 @@ struct SolidityDoc { context: DocContext, } -#[derive(Debug)] +#[derive(Debug, Default)] struct DocContext { parent: Option, } -impl Default for DocContext { - fn default() -> Self { - Self { parent: None } - } -} - #[derive(Debug, PartialEq)] struct SolidityDocPart { comments: Vec, From 0bc2b505a96dfec726128580712c19c3e8997a7a Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Thu, 27 Oct 2022 15:20:37 +0300 Subject: [PATCH 16/67] add struct doc support --- doc/src/as_code.rs | 16 +++++++++++++++- doc/src/builder.rs | 16 +++++++++++++++- doc/src/format.rs | 8 +++++++- doc/src/lib.rs | 13 ++++++++++--- 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/doc/src/as_code.rs b/doc/src/as_code.rs index 87e7102d7a3d..6c186447e80b 100644 --- a/doc/src/as_code.rs +++ b/doc/src/as_code.rs @@ -2,7 +2,8 @@ use forge_fmt::solang_ext::AttrSortKeyIteratorExt; use itertools::Itertools; use solang_parser::pt::{ EventDefinition, EventParameter, Expression, FunctionAttribute, FunctionDefinition, - IdentifierPath, Loc, Parameter, Type, VariableAttribute, VariableDefinition, + IdentifierPath, Loc, Parameter, StructDefinition, Type, VariableAttribute, VariableDeclaration, + VariableDefinition, }; pub trait AsCode { @@ -176,3 +177,16 @@ impl AsCode for IdentifierPath { self.identifiers.iter().map(|ident| ident.name.to_owned()).join(".") } } + +impl AsCode for StructDefinition { + fn as_code(&self) -> String { + let fields = self.fields.iter().map(AsCode::as_code).join(";\n\t"); + format!("struct {} {{\n\t{};\n}}", self.name.name, fields) + } +} + +impl AsCode for VariableDeclaration { + fn as_code(&self) -> String { + format!("{} {}", self.ty.as_code(), self.name.name) + } +} diff --git a/doc/src/builder.rs b/doc/src/builder.rs index 0689efea78ac..f085ea641de9 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -97,6 +97,7 @@ impl DocBuilder { let mut filenames = vec![]; for (path, doc) in docs.as_ref() { for part in doc.iter() { + // TODO: other top level elements if let SolidityDocPartElement::Contract(ref contract) = part.element { let mut doc_file = String::new(); writeln_doc!(doc_file, DocOutput::H1(&contract.name.name))?; @@ -125,6 +126,7 @@ impl DocBuilder { let mut attributes = vec![]; let mut funcs = vec![]; let mut events = vec![]; + let mut structs = vec![]; for child in part.children.iter() { // TODO: remove `clone`s @@ -138,7 +140,10 @@ impl DocBuilder { SolidityDocPartElement::Event(event) => { events.push((event, &child.comments)) } - _ => {} + SolidityDocPartElement::Struct(structure) => { + structs.push((structure, &child.comments)) + } + _ => (), } } @@ -207,6 +212,15 @@ impl DocBuilder { } } + if !structs.is_empty() { + writeln_doc!(doc_file, DocOutput::H2("Structs"))?; + for (structure, comments) in structs { + writeln_doc!(doc_file, "{}\n{}\n", structure, comments)?; + writeln_code!(doc_file, "{}", structure)?; + writeln!(doc_file)?; + } + } + let new_path = path.strip_prefix(&self.config.root)?.to_owned(); fs::create_dir_all(self.out_dir_src().join(&new_path))?; let new_path = new_path.join(&format!("contract.{}.md", contract.name)); diff --git a/doc/src/format.rs b/doc/src/format.rs index 8291a2482f91..7687ca3288cf 100644 --- a/doc/src/format.rs +++ b/doc/src/format.rs @@ -2,7 +2,7 @@ use crate::output::DocOutput; use itertools::Itertools; use solang_parser::{ doccomment::DocCommentTag, - pt::{Base, EventDefinition, FunctionDefinition, VariableDefinition}, + pt::{Base, EventDefinition, FunctionDefinition, StructDefinition, VariableDefinition}, }; pub trait DocFormat { @@ -76,3 +76,9 @@ impl DocFormat for EventDefinition { DocOutput::H3(&self.name.name).doc() } } + +impl DocFormat for StructDefinition { + fn doc(&self) -> String { + DocOutput::H3(&self.name.name).doc() + } +} diff --git a/doc/src/lib.rs b/doc/src/lib.rs index 3dbb0252c494..5611d938df0c 100644 --- a/doc/src/lib.rs +++ b/doc/src/lib.rs @@ -3,7 +3,7 @@ use solang_parser::{ doccomment::{parse_doccomments, DocComment, DocCommentTag}, pt::{ Comment, ContractDefinition, EventDefinition, FunctionDefinition, Loc, SourceUnit, - SourceUnitPart, VariableDefinition, + SourceUnitPart, StructDefinition, VariableDefinition, }, }; use thiserror::Error; @@ -16,7 +16,7 @@ mod macros; mod output; #[derive(Error, Debug)] -enum DocError {} // TODO: +enum DocError {} type Result = std::result::Result; @@ -87,6 +87,7 @@ enum SolidityDocPartElement { Function(FunctionDefinition), Variable(VariableDefinition), Event(EventDefinition), + Struct(StructDefinition), } impl Visitor for SolidityDoc { @@ -134,7 +135,13 @@ impl Visitor for SolidityDoc { Ok(()) } - // TODO: structs + fn visit_struct(&mut self, structure: &mut StructDefinition) -> Result<()> { + self.add_element_to_parent( + SolidityDocPartElement::Struct(structure.clone()), + structure.loc, + ); + Ok(()) + } } #[cfg(test)] From 7b22362630543b2bab98629db5c807a3ed51c89f Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Thu, 27 Oct 2022 15:52:13 +0300 Subject: [PATCH 17/67] clean up & docs --- cli/src/cmd/forge/doc.rs | 2 +- doc/src/as_code.rs | 5 +- doc/src/builder.rs | 72 ++++++---------- doc/src/config.rs | 38 +++++++++ doc/src/format.rs | 3 +- doc/src/helpers.rs | 9 +- doc/src/lib.rs | 173 ++++----------------------------------- doc/src/output.rs | 2 +- doc/src/parser.rs | 154 ++++++++++++++++++++++++++++++++++ 9 files changed, 247 insertions(+), 211 deletions(-) create mode 100644 doc/src/config.rs create mode 100644 doc/src/parser.rs diff --git a/cli/src/cmd/forge/doc.rs b/cli/src/cmd/forge/doc.rs index 4a409e62d494..98ffd2e6d9ca 100644 --- a/cli/src/cmd/forge/doc.rs +++ b/cli/src/cmd/forge/doc.rs @@ -1,6 +1,6 @@ use crate::cmd::Cmd; use clap::{Parser, ValueHint}; -use forge_doc::builder::{DocBuilder, DocConfig}; +use forge_doc::{DocBuilder, DocConfig}; use foundry_config::{find_project_root_path, load_config_with_root}; use std::path::PathBuf; diff --git a/doc/src/as_code.rs b/doc/src/as_code.rs index 6c186447e80b..8243665726a2 100644 --- a/doc/src/as_code.rs +++ b/doc/src/as_code.rs @@ -6,7 +6,10 @@ use solang_parser::pt::{ VariableDefinition, }; -pub trait AsCode { +/// Display Solidity parse tree unit as code string. +/// [AsCode::as_code] formats the unit into +/// a valid Solidity code block. +pub(crate) trait AsCode { fn as_code(&self) -> String; } diff --git a/doc/src/builder.rs b/doc/src/builder.rs index f085ea641de9..20bfcf64cda4 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -1,3 +1,4 @@ +use ethers_solc::utils::source_files_iter; use eyre; use forge_fmt::Visitable; use itertools::Itertools; @@ -15,59 +16,42 @@ use std::{ }; use crate::{ - as_code::AsCode, format::DocFormat, helpers::*, macros::*, output::DocOutput, SolidityDoc, - SolidityDocPart, SolidityDocPartElement, + as_code::AsCode, + config::DocConfig, + format::DocFormat, + helpers::*, + macros::*, + output::DocOutput, + parser::{DocElement, DocParser, DocPart}, }; +/// Build Solidity documentation for a project from natspec comments. +/// The builder parses the source files using [DocParser], +/// then formats and writes the elements as the output. +#[derive(Debug)] pub struct DocBuilder { config: DocConfig, } -// TODO: move & merge w/ Figment -#[derive(Debug)] -pub struct DocConfig { - pub root: PathBuf, - pub sources: PathBuf, - pub out: PathBuf, - pub title: String, -} - -impl DocConfig { - pub fn new(root: &Path) -> Self { - DocConfig { root: root.to_owned(), ..Default::default() } - } - - fn out_dir(&self) -> PathBuf { - self.root.join(&self.out) - } -} - -impl Default for DocConfig { - fn default() -> Self { - DocConfig { - root: PathBuf::new(), - sources: PathBuf::new(), - out: PathBuf::from("docs"), - title: "".to_owned(), - } - } -} - impl DocBuilder { + /// Construct a new builder with default configuration. pub fn new() -> Self { DocBuilder { config: DocConfig::default() } } + /// Construct a new builder with provided configuration pub fn from_config(config: DocConfig) -> Self { DocBuilder { config } } + /// Get the output directory for generated doc files. pub fn out_dir_src(&self) -> PathBuf { self.config.out_dir().join("src") } + /// Parse the sources and build the documentation. pub fn build(self) -> eyre::Result<()> { - let sources: Vec<_> = ethers_solc::utils::source_files_iter(&self.config.sources).collect(); + let sources: Vec<_> = source_files_iter(&self.config.sources).collect(); let docs = sources .par_iter() .enumerate() @@ -81,7 +65,7 @@ impl DocBuilder { diags ) })?; - let mut doc = SolidityDoc::new(comments); + let mut doc = DocParser::new(comments); source_unit.visit(&mut doc)?; Ok((path.clone(), doc.parts)) @@ -98,7 +82,7 @@ impl DocBuilder { for (path, doc) in docs.as_ref() { for part in doc.iter() { // TODO: other top level elements - if let SolidityDocPartElement::Contract(ref contract) = part.element { + if let DocElement::Contract(ref contract) = part.element { let mut doc_file = String::new(); writeln_doc!(doc_file, DocOutput::H1(&contract.name.name))?; @@ -131,16 +115,10 @@ impl DocBuilder { for child in part.children.iter() { // TODO: remove `clone`s match &child.element { - SolidityDocPartElement::Function(func) => { - funcs.push((func, &child.comments)) - } - SolidityDocPartElement::Variable(var) => { - attributes.push((var, &child.comments)) - } - SolidityDocPartElement::Event(event) => { - events.push((event, &child.comments)) - } - SolidityDocPartElement::Struct(structure) => { + DocElement::Function(func) => funcs.push((func, &child.comments)), + DocElement::Variable(var) => attributes.push((var, &child.comments)), + DocElement::Event(event) => events.push((event, &child.comments)), + DocElement::Struct(structure) => { structs.push((structure, &child.comments)) } _ => (), @@ -404,12 +382,12 @@ impl DocBuilder { fn lookup_contract_base<'a>( &self, - docs: &[(PathBuf, Vec)], + docs: &[(PathBuf, Vec)], base: &Base, ) -> eyre::Result> { for (base_path, base_doc) in docs { for base_part in base_doc.iter() { - if let SolidityDocPartElement::Contract(base_contract) = &base_part.element { + if let DocElement::Contract(base_contract) = &base_part.element { if base.name.identifiers.last().unwrap().name == base_contract.name.name { let path = PathBuf::from("/").join( base_path diff --git a/doc/src/config.rs b/doc/src/config.rs new file mode 100644 index 000000000000..5e64cbefd46d --- /dev/null +++ b/doc/src/config.rs @@ -0,0 +1,38 @@ +use std::path::{Path, PathBuf}; + +// TODO: move & merge w/ Figment +/// The doc builder configuration +#[derive(Debug)] +pub struct DocConfig { + /// The project root + pub root: PathBuf, + /// Path to Solidity source files. + pub sources: PathBuf, + /// Output path. + pub out: PathBuf, + /// The documentation title. + pub title: String, +} + +impl DocConfig { + /// Construct new documentation + pub fn new(root: &Path) -> Self { + DocConfig { root: root.to_owned(), ..Default::default() } + } + + /// Get the output directory + pub fn out_dir(&self) -> PathBuf { + self.root.join(&self.out) + } +} + +impl Default for DocConfig { + fn default() -> Self { + DocConfig { + root: PathBuf::new(), + sources: PathBuf::new(), + out: PathBuf::from("docs"), + title: "".to_owned(), + } + } +} diff --git a/doc/src/format.rs b/doc/src/format.rs index 7687ca3288cf..9de7e77bc8ff 100644 --- a/doc/src/format.rs +++ b/doc/src/format.rs @@ -5,7 +5,8 @@ use solang_parser::{ pt::{Base, EventDefinition, FunctionDefinition, StructDefinition, VariableDefinition}, }; -pub trait DocFormat { +/// TODO: +pub(crate) trait DocFormat { fn doc(&self) -> String; } diff --git a/doc/src/helpers.rs b/doc/src/helpers.rs index 976937e4dae2..8ae4ae75f5e6 100644 --- a/doc/src/helpers.rs +++ b/doc/src/helpers.rs @@ -1,14 +1,17 @@ use solang_parser::doccomment::DocCommentTag; -/// TODO: -pub fn filter_comments_by_tag<'a>( +/// Filter a collection of comments and return +/// only those that match a given tag +pub(crate) fn filter_comments_by_tag<'a>( comments: &'a [DocCommentTag], tag: &str, ) -> Vec<&'a DocCommentTag> { comments.iter().filter(|c| c.tag == tag).collect() } -pub fn filter_comments_without_tags<'a>( +/// Filter a collection of comments and return +/// only those that do not have provided tags +pub(crate) fn filter_comments_without_tags<'a>( comments: &'a [DocCommentTag], tags: Vec<&str>, ) -> Vec<&'a DocCommentTag> { diff --git a/doc/src/lib.rs b/doc/src/lib.rs index 5611d938df0c..cfa0b6fc55b6 100644 --- a/doc/src/lib.rs +++ b/doc/src/lib.rs @@ -1,163 +1,22 @@ -use forge_fmt::{Visitable, Visitor}; -use solang_parser::{ - doccomment::{parse_doccomments, DocComment, DocCommentTag}, - pt::{ - Comment, ContractDefinition, EventDefinition, FunctionDefinition, Loc, SourceUnit, - SourceUnitPart, StructDefinition, VariableDefinition, - }, -}; -use thiserror::Error; +#![warn(missing_debug_implementations, missing_docs, unreachable_pub)] +#![deny(unused_must_use, rust_2018_idioms)] +#![doc(test( + no_crate_inject, + attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_variables)) +))] + +//! The module for generating Solidity documentation. +//! +//! See [DocBuilder] + +pub use builder::DocBuilder; +pub use config::DocConfig; mod as_code; -pub mod builder; +mod builder; +mod config; mod format; mod helpers; mod macros; mod output; - -#[derive(Error, Debug)] -enum DocError {} - -type Result = std::result::Result; - -#[derive(Debug)] -struct SolidityDoc { - parts: Vec, - comments: Vec, - start_at: usize, - context: DocContext, -} - -#[derive(Debug, Default)] -struct DocContext { - parent: Option, -} - -#[derive(Debug, PartialEq)] -struct SolidityDocPart { - comments: Vec, - element: SolidityDocPartElement, - children: Vec, -} - -impl SolidityDoc { - fn new(comments: Vec) -> Self { - SolidityDoc { parts: vec![], comments, start_at: 0, context: Default::default() } - } - - fn with_parent( - &mut self, - mut parent: SolidityDocPart, - mut visit: impl FnMut(&mut Self) -> Result<()>, - ) -> Result { - let curr = self.context.parent.take(); - self.context.parent = Some(parent); - visit(self)?; - parent = self.context.parent.take().unwrap(); - self.context.parent = curr; - Ok(parent) - } - - fn add_element_to_parent(&mut self, element: SolidityDocPartElement, loc: Loc) { - let child = - SolidityDocPart { comments: self.parse_docs(loc.start()), element, children: vec![] }; - if let Some(parent) = self.context.parent.as_mut() { - parent.children.push(child); - } else { - self.parts.push(child); - } - self.start_at = loc.end(); - } - - fn parse_docs(&mut self, end: usize) -> Vec { - let mut res = vec![]; - for comment in parse_doccomments(&self.comments, self.start_at, end) { - match comment { - DocComment::Line { comment } => res.push(comment), - DocComment::Block { comments } => res.extend(comments.into_iter()), - } - } - res - } -} - -#[derive(Debug, PartialEq)] -enum SolidityDocPartElement { - Contract(Box), - Function(FunctionDefinition), - Variable(VariableDefinition), - Event(EventDefinition), - Struct(StructDefinition), -} - -impl Visitor for SolidityDoc { - type Error = DocError; - - fn visit_source_unit(&mut self, source_unit: &mut SourceUnit) -> Result<()> { - for source in source_unit.0.iter_mut() { - match source { - SourceUnitPart::ContractDefinition(def) => { - let contract = SolidityDocPart { - element: SolidityDocPartElement::Contract(def.clone()), - comments: self.parse_docs(def.loc.start()), - children: vec![], - }; - self.start_at = def.loc.start(); - let contract = self.with_parent(contract, |doc| { - def.parts.iter_mut().map(|d| d.visit(doc)).collect::>>()?; - Ok(()) - })?; - - self.start_at = def.loc.end(); - self.parts.push(contract); - } - SourceUnitPart::FunctionDefinition(func) => self.visit_function(func)?, - SourceUnitPart::EventDefinition(event) => self.visit_event(event)?, - _ => {} - }; - } - - Ok(()) - } - - fn visit_function(&mut self, func: &mut FunctionDefinition) -> Result<()> { - self.add_element_to_parent(SolidityDocPartElement::Function(func.clone()), func.loc); - Ok(()) - } - - fn visit_var_definition(&mut self, var: &mut VariableDefinition) -> Result<()> { - self.add_element_to_parent(SolidityDocPartElement::Variable(var.clone()), var.loc); - Ok(()) - } - - fn visit_event(&mut self, event: &mut EventDefinition) -> Result<()> { - self.add_element_to_parent(SolidityDocPartElement::Event(event.clone()), event.loc); - Ok(()) - } - - fn visit_struct(&mut self, structure: &mut StructDefinition) -> Result<()> { - self.add_element_to_parent( - SolidityDocPartElement::Struct(structure.clone()), - structure.loc, - ); - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::{fs, path::PathBuf}; - - #[test] - fn parse_docs() { - let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("testdata") - .join("oz-governance") - .join("Governor.sol"); - let target = fs::read_to_string(path).unwrap(); - let (mut source_pt, source_comments) = solang_parser::parse(&target, 1).unwrap(); - let mut d = SolidityDoc::new(source_comments); - assert!(source_pt.visit(&mut d).is_ok()); - } -} +mod parser; diff --git a/doc/src/output.rs b/doc/src/output.rs index 0fbb239589eb..b36450d05f26 100644 --- a/doc/src/output.rs +++ b/doc/src/output.rs @@ -1,6 +1,6 @@ use crate::format::DocFormat; -pub enum DocOutput<'a> { +pub(crate) enum DocOutput<'a> { H1(&'a str), H2(&'a str), H3(&'a str), diff --git a/doc/src/parser.rs b/doc/src/parser.rs new file mode 100644 index 000000000000..bdf7a7ea39d1 --- /dev/null +++ b/doc/src/parser.rs @@ -0,0 +1,154 @@ +use forge_fmt::{Visitable, Visitor}; +use solang_parser::{ + doccomment::{parse_doccomments, DocComment, DocCommentTag}, + pt::{ + Comment, ContractDefinition, EventDefinition, FunctionDefinition, Loc, SourceUnit, + SourceUnitPart, StructDefinition, VariableDefinition, + }, +}; +use thiserror::Error; + +/// The parser error +#[derive(Error, Debug)] +pub(crate) enum DocParserError {} + +type Result = std::result::Result; + +#[derive(Debug)] +pub(crate) struct DocParser { + pub(crate) parts: Vec, + comments: Vec, + start_at: usize, + context: DocContext, +} + +#[derive(Debug, Default)] +struct DocContext { + parent: Option, +} + +#[derive(Debug, PartialEq)] +pub(crate) struct DocPart { + pub(crate) comments: Vec, + pub(crate) element: DocElement, + pub(crate) children: Vec, +} + +#[derive(Debug, PartialEq)] +pub(crate) enum DocElement { + Contract(Box), + Function(FunctionDefinition), + Variable(VariableDefinition), + Event(EventDefinition), + Struct(StructDefinition), +} + +impl DocParser { + pub(crate) fn new(comments: Vec) -> Self { + DocParser { parts: vec![], comments, start_at: 0, context: Default::default() } + } + + fn with_parent( + &mut self, + mut parent: DocPart, + mut visit: impl FnMut(&mut Self) -> Result<()>, + ) -> Result { + let curr = self.context.parent.take(); + self.context.parent = Some(parent); + visit(self)?; + parent = self.context.parent.take().unwrap(); + self.context.parent = curr; + Ok(parent) + } + + fn add_element_to_parent(&mut self, element: DocElement, loc: Loc) { + let child = DocPart { comments: self.parse_docs(loc.start()), element, children: vec![] }; + if let Some(parent) = self.context.parent.as_mut() { + parent.children.push(child); + } else { + self.parts.push(child); + } + self.start_at = loc.end(); + } + + fn parse_docs(&mut self, end: usize) -> Vec { + let mut res = vec![]; + for comment in parse_doccomments(&self.comments, self.start_at, end) { + match comment { + DocComment::Line { comment } => res.push(comment), + DocComment::Block { comments } => res.extend(comments.into_iter()), + } + } + res + } +} + +impl Visitor for DocParser { + type Error = DocParserError; + + fn visit_source_unit(&mut self, source_unit: &mut SourceUnit) -> Result<()> { + for source in source_unit.0.iter_mut() { + match source { + SourceUnitPart::ContractDefinition(def) => { + let contract = DocPart { + element: DocElement::Contract(def.clone()), + comments: self.parse_docs(def.loc.start()), + children: vec![], + }; + self.start_at = def.loc.start(); + let contract = self.with_parent(contract, |doc| { + def.parts.iter_mut().map(|d| d.visit(doc)).collect::>>()?; + Ok(()) + })?; + + self.start_at = def.loc.end(); + self.parts.push(contract); + } + SourceUnitPart::FunctionDefinition(func) => self.visit_function(func)?, + SourceUnitPart::EventDefinition(event) => self.visit_event(event)?, + SourceUnitPart::StructDefinition(structure) => self.visit_struct(structure)?, + _ => {} + }; + } + + Ok(()) + } + + fn visit_function(&mut self, func: &mut FunctionDefinition) -> Result<()> { + self.add_element_to_parent(DocElement::Function(func.clone()), func.loc); + Ok(()) + } + + fn visit_var_definition(&mut self, var: &mut VariableDefinition) -> Result<()> { + self.add_element_to_parent(DocElement::Variable(var.clone()), var.loc); + Ok(()) + } + + fn visit_event(&mut self, event: &mut EventDefinition) -> Result<()> { + self.add_element_to_parent(DocElement::Event(event.clone()), event.loc); + Ok(()) + } + + fn visit_struct(&mut self, structure: &mut StructDefinition) -> Result<()> { + self.add_element_to_parent(DocElement::Struct(structure.clone()), structure.loc); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::{fs, path::PathBuf}; + + #[test] + fn parse_docs() { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("testdata") + .join("oz-governance") + .join("Governor.sol"); + let target = fs::read_to_string(path).unwrap(); + let (mut source_pt, source_comments) = solang_parser::parse(&target, 1).unwrap(); + let mut d = DocParser::new(source_comments); + assert!(source_pt.visit(&mut d).is_ok()); + } +} From eb4037dede51a158144d783ada7e095396b43874 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Fri, 28 Oct 2022 09:40:09 +0300 Subject: [PATCH 18/67] remove ty from filenames --- doc/src/builder.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/doc/src/builder.rs b/doc/src/builder.rs index 20bfcf64cda4..6b1a2a9d937c 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -5,7 +5,7 @@ use itertools::Itertools; use rayon::prelude::*; use solang_parser::{ doccomment::DocCommentTag, - pt::{Base, ContractTy, Identifier, Parameter}, + pt::{Base, Identifier, Parameter}, }; use std::{ cmp::Ordering, @@ -204,7 +204,7 @@ impl DocBuilder { let new_path = new_path.join(&format!("contract.{}.md", contract.name)); fs::write(self.out_dir_src().join(&new_path), doc_file)?; - filenames.push((contract.name.clone(), contract.ty.clone(), new_path)); + filenames.push((contract.name.clone(), new_path)); } } } @@ -252,10 +252,7 @@ impl DocBuilder { Ok(out) } - fn write_book_config( - &self, - filenames: Vec<(Identifier, ContractTy, PathBuf)>, - ) -> eyre::Result<()> { + fn write_book_config(&self, filenames: Vec<(Identifier, PathBuf)>) -> eyre::Result<()> { let out_dir_src = self.out_dir_src(); let readme_content = { @@ -297,7 +294,7 @@ impl DocBuilder { out: &mut String, depth: usize, path: Option<&Path>, - entries: &[(Identifier, ContractTy, PathBuf)], + entries: &[(Identifier, PathBuf)], ) -> eyre::Result<()> { if !entries.is_empty() { if let Some(path) = path { @@ -320,7 +317,7 @@ impl DocBuilder { // group and sort entries let mut grouped = HashMap::new(); for entry in entries { - let key = entry.2.iter().take(depth + 1).collect::(); + let key = entry.1.iter().take(depth + 1).collect::(); grouped.entry(key).or_insert_with(Vec::new).push(entry.clone()); } let grouped = grouped.iter().sorted_by(|(lhs, _), (rhs, _)| { @@ -339,7 +336,7 @@ impl DocBuilder { let mut readme = String::from("\n\n# Contents\n"); for (path, entries) in grouped { if path.extension().map(|ext| ext.eq("sol")).unwrap_or_default() { - for (src, _, filename) in entries { + for (src, filename) in entries { writeln_doc!( readme, "- {}", From af453e7e4cbb265c782ea7d6ac1701e49aecec87 Mon Sep 17 00:00:00 2001 From: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Date: Fri, 4 Nov 2022 14:02:02 +0200 Subject: [PATCH 19/67] feat(doc): support errors, enums & render top level elements (#3565) * add error and enum doc support * render top level elements * fix: enum format & grouped DocElements * feat: add format_section function * fix: linting * fix: return table values --- doc/src/as_code.rs | 42 +++++- doc/src/builder.rs | 360 ++++++++++++++++++++++++++++++++------------- doc/src/format.rs | 17 ++- doc/src/parser.rs | 18 ++- 4 files changed, 329 insertions(+), 108 deletions(-) diff --git a/doc/src/as_code.rs b/doc/src/as_code.rs index 8243665726a2..04ca5ceba494 100644 --- a/doc/src/as_code.rs +++ b/doc/src/as_code.rs @@ -1,9 +1,9 @@ use forge_fmt::solang_ext::AttrSortKeyIteratorExt; use itertools::Itertools; use solang_parser::pt::{ - EventDefinition, EventParameter, Expression, FunctionAttribute, FunctionDefinition, - IdentifierPath, Loc, Parameter, StructDefinition, Type, VariableAttribute, VariableDeclaration, - VariableDefinition, + EnumDefinition, ErrorDefinition, ErrorParameter, EventDefinition, EventParameter, Expression, + FunctionAttribute, FunctionDefinition, Identifier, IdentifierPath, Loc, Parameter, + StructDefinition, Type, VariableAttribute, VariableDeclaration, VariableDefinition, }; /// Display Solidity parse tree unit as code string. @@ -175,6 +175,28 @@ impl AsCode for Vec { } } +impl AsCode for ErrorDefinition { + fn as_code(&self) -> String { + let name = &self.name.name; + let fields = self.fields.as_code(); + format!("error {name}({fields})") + } +} + +impl AsCode for ErrorParameter { + fn as_code(&self) -> String { + let ty = self.ty.as_code(); + let name = self.name.as_ref().map(|name| name.name.to_owned()).unwrap_or_default(); + format!("{ty} {name}") + } +} + +impl AsCode for Vec { + fn as_code(&self) -> String { + self.iter().map(AsCode::as_code).join(", ") + } +} + impl AsCode for IdentifierPath { fn as_code(&self) -> String { self.identifiers.iter().map(|ident| ident.name.to_owned()).join(".") @@ -193,3 +215,17 @@ impl AsCode for VariableDeclaration { format!("{} {}", self.ty.as_code(), self.name.name) } } + +impl AsCode for EnumDefinition { + fn as_code(&self) -> String { + let name = &self.name.name; + let values = self.values.iter().map(AsCode::as_code).join("\n\t"); + format!("enum {name}{{\n\t{values}\n}}") + } +} + +impl AsCode for Identifier { + fn as_code(&self) -> String { + format!("{}", self.name) + } +} diff --git a/doc/src/builder.rs b/doc/src/builder.rs index 6b1a2a9d937c..9aae463a16a2 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -81,130 +81,269 @@ impl DocBuilder { let mut filenames = vec![]; for (path, doc) in docs.as_ref() { for part in doc.iter() { - // TODO: other top level elements - if let DocElement::Contract(ref contract) = part.element { - let mut doc_file = String::new(); - writeln_doc!(doc_file, DocOutput::H1(&contract.name.name))?; - - if !contract.base.is_empty() { - let bases = contract - .base - .iter() - .map(|base| { - Ok(self - .lookup_contract_base(docs.as_ref(), base)? - .unwrap_or(base.doc())) - }) - .collect::>>()?; + match part.element { + DocElement::Contract(ref contract) => { + let mut doc_file = String::new(); + writeln_doc!(doc_file, DocOutput::H1(&contract.name.name))?; + + if !contract.base.is_empty() { + let bases = contract + .base + .iter() + .map(|base| { + Ok(self + .lookup_contract_base(docs.as_ref(), base)? + .unwrap_or(base.doc())) + }) + .collect::>>()?; - writeln_doc!( - doc_file, - "{} {}\n", - DocOutput::Bold("Inherits:"), - bases.join(", ") - )?; - } - - writeln_doc!(doc_file, part.comments)?; - - let mut attributes = vec![]; - let mut funcs = vec![]; - let mut events = vec![]; - let mut structs = vec![]; - - for child in part.children.iter() { - // TODO: remove `clone`s - match &child.element { - DocElement::Function(func) => funcs.push((func, &child.comments)), - DocElement::Variable(var) => attributes.push((var, &child.comments)), - DocElement::Event(event) => events.push((event, &child.comments)), - DocElement::Struct(structure) => { - structs.push((structure, &child.comments)) - } - _ => (), + writeln_doc!( + doc_file, + "{} {}\n", + DocOutput::Bold("Inherits:"), + bases.join(", ") + )?; } - } - if !attributes.is_empty() { - writeln_doc!(doc_file, DocOutput::H2("Attributes"))?; - for (var, comments) in attributes { - writeln_doc!(doc_file, "{}\n{}\n", var, comments)?; - writeln_code!(doc_file, "{}", var)?; - writeln!(doc_file)?; + writeln_doc!(doc_file, part.comments)?; + + let mut attributes = vec![]; + let mut funcs = vec![]; + let mut events = vec![]; + let mut errors = vec![]; + let mut structs = vec![]; + let mut enumerables = vec![]; + + for child in part.children.iter() { + // TODO: remove `clone`s + match &child.element { + DocElement::Function(func) => funcs.push((func, &child.comments)), + DocElement::Variable(var) => { + attributes.push((var, &child.comments)) + } + DocElement::Event(event) => events.push((event, &child.comments)), + DocElement::Error(error) => errors.push((error, &child.comments)), + DocElement::Struct(structure) => { + structs.push((structure, &child.comments)) + } + DocElement::Enum(enumerable) => { + enumerables.push((enumerable, &child.comments)) + } + _ => (), + } } - } - if !funcs.is_empty() { - writeln_doc!(doc_file, DocOutput::H2("Functions"))?; - for (func, comments) in funcs { - writeln_doc!(doc_file, "{}\n", func)?; + if !attributes.is_empty() { writeln_doc!( doc_file, - "{}\n", - filter_comments_without_tags(&comments, vec!["param", "return"]) + "{}", + self.format_section("Attributes", attributes)? )?; - writeln_code!(doc_file, "{}", func)?; + } - let params: Vec<_> = - func.params.iter().filter_map(|p| p.1.as_ref()).collect(); - let param_comments = filter_comments_by_tag(&comments, "param"); - if !params.is_empty() && !param_comments.is_empty() { + if !funcs.is_empty() { + writeln_doc!(doc_file, DocOutput::H2("Functions"))?; + for (func, comments) in funcs { + writeln_doc!(doc_file, "{}\n", func)?; writeln_doc!( doc_file, - "{}\n{}", - DocOutput::H3("Parameters"), - self.format_comment_table( - &["Name", "Type", "Description"], - ¶ms, - ¶m_comments - )? + "{}\n", + filter_comments_without_tags( + &comments, + vec!["param", "return"] + ) )?; + writeln_code!(doc_file, "{}", func)?; + + let params: Vec<_> = + func.params.iter().filter_map(|p| p.1.as_ref()).collect(); + let param_comments = filter_comments_by_tag(&comments, "param"); + if !params.is_empty() && !param_comments.is_empty() { + writeln_doc!( + doc_file, + "{}\n{}", + DocOutput::H3("Parameters"), + self.format_comment_table( + &["Name", "Type", "Description"], + ¶ms, + ¶m_comments + )? + )?; + } + + let returns: Vec<_> = + func.returns.iter().filter_map(|p| p.1.as_ref()).collect(); + let returns_comments = filter_comments_by_tag(&comments, "return"); + if !returns.is_empty() && !returns_comments.is_empty() { + writeln_doc!( + doc_file, + "{}\n{}", + DocOutput::H3("Returns"), + self.format_comment_table( + &["Name", "Type", "Description"], + &returns, + &returns_comments + )? + )?; + } + + writeln!(doc_file)?; } + } - let returns: Vec<_> = - func.returns.iter().filter_map(|p| p.1.as_ref()).collect(); - let returns_comments = filter_comments_by_tag(&comments, "return"); - if !returns.is_empty() && !returns_comments.is_empty() { - writeln_doc!( - doc_file, - "{}\n{}", - DocOutput::H3("Returns"), - self.format_comment_table( - &["Name", "Type", "Description"], - ¶ms, - &returns_comments - )? - )?; - } + if !events.is_empty() { + writeln_doc!(doc_file, "{}", self.format_section("Events", events)?)?; + } - writeln!(doc_file)?; + if !errors.is_empty() { + writeln_doc!(doc_file, "{}", self.format_section("Errors", errors)?)?; } - } - if !events.is_empty() { - writeln_doc!(doc_file, DocOutput::H2("Events"))?; - for (ev, comments) in events { - writeln_doc!(doc_file, "{}\n{}\n", ev, comments)?; - writeln_code!(doc_file, "{}", ev)?; - writeln!(doc_file)?; + if !structs.is_empty() { + writeln_doc!(doc_file, "{}", self.format_section("Structs", structs)?)?; } - } - if !structs.is_empty() { - writeln_doc!(doc_file, DocOutput::H2("Structs"))?; - for (structure, comments) in structs { - writeln_doc!(doc_file, "{}\n{}\n", structure, comments)?; - writeln_code!(doc_file, "{}", structure)?; - writeln!(doc_file)?; + if !enumerables.is_empty() { + writeln_doc!( + doc_file, + "{}", + self.format_section("Enums", enumerables)? + )?; } + + let new_path = path.strip_prefix(&self.config.root)?.to_owned(); + fs::create_dir_all(self.out_dir_src().join(&new_path))?; + let new_path = new_path.join(&format!("contract.{}.md", contract.name)); + + fs::write(self.out_dir_src().join(&new_path), doc_file)?; + filenames.push((contract.name.clone(), new_path)); + } + DocElement::Variable(ref attribute) => { + let mut doc_file = String::new(); + let mut variable_section = vec![]; + variable_section.push((attribute, &part.comments)); + writeln_doc!( + doc_file, + "{}", + self.format_section("Attribute", variable_section)? + )?; + + let new_path = path.strip_prefix(&self.config.root)?.to_owned(); + fs::create_dir_all(self.out_dir_src().join(&new_path))?; + let new_path = new_path.join(&format!("variable.{}.md", attribute.name)); + + fs::write(self.out_dir_src().join(&new_path), doc_file)?; + filenames.push((attribute.name.clone(), new_path)); + } + DocElement::Error(ref error) => { + let mut doc_file = String::new(); + let mut error_section = vec![]; + error_section.push((error, &part.comments)); + writeln_doc!(doc_file, "{}", self.format_section("Error", error_section)?)?; + + let new_path = path.strip_prefix(&self.config.root)?.to_owned(); + fs::create_dir_all(self.out_dir_src().join(&new_path))?; + let new_path = new_path.join(&format!("error.{}.md", error.name)); + + fs::write(self.out_dir_src().join(&new_path), doc_file)?; + filenames.push((error.name.clone(), new_path)); + } + DocElement::Event(ref event) => { + let mut doc_file = String::new(); + let mut event_section = vec![]; + event_section.push((event, &part.comments)); + writeln_doc!(doc_file, "{}", self.format_section("Event", event_section)?)?; + + let new_path = path.strip_prefix(&self.config.root)?.to_owned(); + fs::create_dir_all(self.out_dir_src().join(&new_path))?; + let new_path = new_path.join(&format!("event.{}.md", event.name)); + + fs::write(self.out_dir_src().join(&new_path), doc_file)?; + filenames.push((event.name.clone(), new_path)); + } + DocElement::Struct(ref structure) => { + let mut doc_file = String::new(); + let mut struct_section = vec![]; + struct_section.push((structure, &part.comments)); + writeln_doc!( + doc_file, + "{}", + self.format_section("Struct", struct_section)? + )?; + + let new_path = path.strip_prefix(&self.config.root)?.to_owned(); + fs::create_dir_all(self.out_dir_src().join(&new_path))?; + let new_path = new_path.join(&format!("struct.{}.md", structure.name)); + + fs::write(self.out_dir_src().join(&new_path), doc_file)?; + filenames.push((structure.name.clone(), new_path)); + } + DocElement::Enum(ref enumerable) => { + let mut doc_file = String::new(); + let mut enum_section = vec![]; + enum_section.push((enumerable, &part.comments)); + writeln_doc!(doc_file, "{}", self.format_section("Enum", enum_section)?)?; + + let new_path = path.strip_prefix(&self.config.root)?.to_owned(); + fs::create_dir_all(self.out_dir_src().join(&new_path))?; + let new_path = new_path.join(&format!("enum.{}.md", enumerable.name)); + + fs::write(self.out_dir_src().join(&new_path), doc_file)?; + filenames.push((enumerable.name.clone(), new_path)); } + DocElement::Function(ref func) => { + let mut doc_file = String::new(); + writeln_doc!(doc_file, DocOutput::H1("Function"))?; - let new_path = path.strip_prefix(&self.config.root)?.to_owned(); - fs::create_dir_all(self.out_dir_src().join(&new_path))?; - let new_path = new_path.join(&format!("contract.{}.md", contract.name)); + writeln_doc!(doc_file, "{}\n", func)?; + writeln_doc!( + doc_file, + "{}\n", + filter_comments_without_tags(&part.comments, vec!["param", "return"]) + )?; + writeln_code!(doc_file, "{}", func)?; - fs::write(self.out_dir_src().join(&new_path), doc_file)?; - filenames.push((contract.name.clone(), new_path)); + let params: Vec<_> = + func.params.iter().filter_map(|p| p.1.as_ref()).collect(); + let param_comments = filter_comments_by_tag(&part.comments, "param"); + if !params.is_empty() && !param_comments.is_empty() { + writeln_doc!( + doc_file, + "{}\n{}", + DocOutput::H3("Parameters"), + self.format_comment_table( + &["Name", "Type", "Description"], + ¶ms, + ¶m_comments + )? + )?; + } + + let returns: Vec<_> = + func.returns.iter().filter_map(|p| p.1.as_ref()).collect(); + let returns_comments = filter_comments_by_tag(&part.comments, "return"); + if !returns.is_empty() && !returns_comments.is_empty() { + writeln_doc!( + doc_file, + "{}\n{}", + DocOutput::H3("Returns"), + self.format_comment_table( + &["Name", "Type", "Description"], + &returns, + &returns_comments + )? + )?; + } + writeln!(doc_file)?; + let new_path = path.strip_prefix(&self.config.root)?.to_owned(); + fs::create_dir_all(self.out_dir_src().join(&new_path))?; + let func_name = + func.name.as_ref().map(|name| name.name.to_owned()).unwrap_or_default(); + let new_path = new_path.join(&format!("function.{}.md", func_name)); + + fs::write(self.out_dir_src().join(&new_path), doc_file)?; + // filenames.push((func.name.clone(), new_path)); + } } } } @@ -252,6 +391,23 @@ impl DocBuilder { Ok(out) } + fn format_section( + &self, + title: &str, + entries: Vec<(&T, &Vec)>, + ) -> eyre::Result { + let mut out = String::new(); + + writeln!(out, "{}", DocOutput::H2(title))?; + for (ev, comments) in entries { + writeln!(out, "{}\n{}\n", ev.doc(), comments.doc())?; + writeln_code!(out, "{}", ev)?; + writeln!(out)?; + } + + return Ok(out); + } + fn write_book_config(&self, filenames: Vec<(Identifier, PathBuf)>) -> eyre::Result<()> { let out_dir_src = self.out_dir_src(); @@ -393,7 +549,7 @@ impl DocBuilder { ); return Ok(Some( DocOutput::Link(&base.doc(), &path.display().to_string()).doc(), - )) + )); } } } diff --git a/doc/src/format.rs b/doc/src/format.rs index 9de7e77bc8ff..be351dd8921d 100644 --- a/doc/src/format.rs +++ b/doc/src/format.rs @@ -2,7 +2,10 @@ use crate::output::DocOutput; use itertools::Itertools; use solang_parser::{ doccomment::DocCommentTag, - pt::{Base, EventDefinition, FunctionDefinition, StructDefinition, VariableDefinition}, + pt::{ + Base, EnumDefinition, ErrorDefinition, EventDefinition, FunctionDefinition, + StructDefinition, VariableDefinition, + }, }; /// TODO: @@ -78,8 +81,20 @@ impl DocFormat for EventDefinition { } } +impl DocFormat for ErrorDefinition { + fn doc(&self) -> String { + DocOutput::H3(&self.name.name).doc() + } +} + impl DocFormat for StructDefinition { fn doc(&self) -> String { DocOutput::H3(&self.name.name).doc() } } + +impl DocFormat for EnumDefinition { + fn doc(&self) -> String { + DocOutput::H3(&self.name.name).doc() + } +} diff --git a/doc/src/parser.rs b/doc/src/parser.rs index bdf7a7ea39d1..972f1e038a41 100644 --- a/doc/src/parser.rs +++ b/doc/src/parser.rs @@ -2,8 +2,8 @@ use forge_fmt::{Visitable, Visitor}; use solang_parser::{ doccomment::{parse_doccomments, DocComment, DocCommentTag}, pt::{ - Comment, ContractDefinition, EventDefinition, FunctionDefinition, Loc, SourceUnit, - SourceUnitPart, StructDefinition, VariableDefinition, + Comment, ContractDefinition, EnumDefinition, ErrorDefinition, EventDefinition, + FunctionDefinition, Loc, SourceUnit, SourceUnitPart, StructDefinition, VariableDefinition, }, }; use thiserror::Error; @@ -40,7 +40,9 @@ pub(crate) enum DocElement { Function(FunctionDefinition), Variable(VariableDefinition), Event(EventDefinition), + Error(ErrorDefinition), Struct(StructDefinition), + Enum(EnumDefinition), } impl DocParser { @@ -106,7 +108,9 @@ impl Visitor for DocParser { } SourceUnitPart::FunctionDefinition(func) => self.visit_function(func)?, SourceUnitPart::EventDefinition(event) => self.visit_event(event)?, + SourceUnitPart::ErrorDefinition(error) => self.visit_error(error)?, SourceUnitPart::StructDefinition(structure) => self.visit_struct(structure)?, + SourceUnitPart::EnumDefinition(enumerable) => self.visit_enum(enumerable)?, _ => {} }; } @@ -129,10 +133,20 @@ impl Visitor for DocParser { Ok(()) } + fn visit_error(&mut self, error: &mut ErrorDefinition) -> Result<()> { + self.add_element_to_parent(DocElement::Error(error.clone()), error.loc); + Ok(()) + } + fn visit_struct(&mut self, structure: &mut StructDefinition) -> Result<()> { self.add_element_to_parent(DocElement::Struct(structure.clone()), structure.loc); Ok(()) } + + fn visit_enum(&mut self, enumerable: &mut EnumDefinition) -> Result<()> { + self.add_element_to_parent(DocElement::Enum(enumerable.clone()), enumerable.loc); + Ok(()) + } } #[cfg(test)] From d9965ef65cab71a96a5efaa5e88c3800634e277c Mon Sep 17 00:00:00 2001 From: 0xOneTony <112496816+0xOneTony@users.noreply.github.com> Date: Wed, 9 Nov 2022 12:36:16 +0200 Subject: [PATCH 20/67] feat(doc): add out path option in config (#3643) * feat(doc): add out path option in config * fix: out unwrap issue * fix: update out config option * fix: formatting --- cli/src/cmd/forge/doc.rs | 11 +++++++++++ doc/src/builder.rs | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/cli/src/cmd/forge/doc.rs b/cli/src/cmd/forge/doc.rs index 98ffd2e6d9ca..163988d3dd8a 100644 --- a/cli/src/cmd/forge/doc.rs +++ b/cli/src/cmd/forge/doc.rs @@ -14,6 +14,16 @@ pub struct DocArgs { value_name = "PATH" )] root: Option, + + #[clap( + help = "The doc's output path.", + long_help = "The path where the docs are gonna get generated. By default, this is gonna be the docs directory at the root of the project.", + long = "out", + short, + value_hint = ValueHint::DirPath, + value_name = "PATH" + )] + out: Option, } impl Cmd for DocArgs { @@ -24,6 +34,7 @@ impl Cmd for DocArgs { DocBuilder::from_config(DocConfig { root: self.root.as_ref().unwrap_or(&find_project_root_path()?).to_path_buf(), sources: config.project_paths().sources, + out: self.out.clone().unwrap_or_else(|| PathBuf::from("docs")), ..Default::default() }) .build() diff --git a/doc/src/builder.rs b/doc/src/builder.rs index 9aae463a16a2..5ba6463e44ee 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -405,7 +405,7 @@ impl DocBuilder { writeln!(out)?; } - return Ok(out); + return Ok(out) } fn write_book_config(&self, filenames: Vec<(Identifier, PathBuf)>) -> eyre::Result<()> { @@ -549,7 +549,7 @@ impl DocBuilder { ); return Ok(Some( DocOutput::Link(&base.doc(), &path.display().to_string()).doc(), - )); + )) } } } From 92d6d77c5681d13ea8fc0acc60a48d7c7c50f43a Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Thu, 5 Jan 2023 19:48:22 +0200 Subject: [PATCH 21/67] rewrite forge doc --- Cargo.lock | 1 + doc/Cargo.toml | 3 +- doc/src/as_code.rs | 84 ++++--- doc/src/builder.rs | 563 +++++++++++---------------------------------- doc/src/format.rs | 258 +++++++++++++++++---- doc/src/helpers.rs | 4 +- doc/src/lib.rs | 2 +- doc/src/macros.rs | 20 -- doc/src/output.rs | 19 +- doc/src/parser.rs | 83 ++++++- doc/src/writer.rs | 129 +++++++++++ 11 files changed, 614 insertions(+), 552 deletions(-) delete mode 100644 doc/src/macros.rs create mode 100644 doc/src/writer.rs diff --git a/Cargo.lock b/Cargo.lock index be904741b4be..57e4ae0bf6e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2129,6 +2129,7 @@ dependencies = [ name = "forge-doc" version = "0.1.0" dependencies = [ + "auto_impl 1.0.1", "clap 3.2.16", "ethers-solc", "eyre", diff --git a/doc/Cargo.toml b/doc/Cargo.toml index 39e951d39bc5..ae2ca58ca443 100644 --- a/doc/Cargo.toml +++ b/doc/Cargo.toml @@ -30,4 +30,5 @@ eyre = "0.6" thiserror = "1.0.30" rayon = "1.5.1" itertools = "0.10.3" -toml = "0.5" \ No newline at end of file +toml = "0.5" +auto_impl = "1" diff --git a/doc/src/as_code.rs b/doc/src/as_code.rs index 04ca5ceba494..55b0be89ed41 100644 --- a/doc/src/as_code.rs +++ b/doc/src/as_code.rs @@ -9,6 +9,7 @@ use solang_parser::pt::{ /// Display Solidity parse tree unit as code string. /// [AsCode::as_code] formats the unit into /// a valid Solidity code block. +#[auto_impl::auto_impl(&)] pub(crate) trait AsCode { fn as_code(&self) -> String; } @@ -58,52 +59,48 @@ impl AsCode for FunctionDefinition { impl AsCode for Expression { fn as_code(&self) -> String { match self { - Expression::Type(_, ty) => { - match ty { - Type::Address => "address".to_owned(), - Type::AddressPayable => "address payable".to_owned(), - Type::Payable => "payable".to_owned(), - Type::Bool => "bool".to_owned(), - Type::String => "string".to_owned(), - Type::Bytes(n) => format!("bytes{}", n), - Type::Rational => "rational".to_owned(), - Type::DynamicBytes => "bytes".to_owned(), - Type::Int(n) => format!("int{}", n), - Type::Uint(n) => format!("uint{}", n), - Type::Mapping(_, from, to) => format!("mapping({} => {})", from.as_code(), to.as_code()), - Type::Function { params, attributes, returns } => { - let params = params.as_code(); - let mut attributes = attributes.as_code(); - if !attributes.is_empty() { - attributes.insert(0, ' '); - } - let mut returns_str = String::new(); - if let Some((params, _attrs)) = returns { - returns_str = params.as_code(); - if !returns_str.is_empty() { - returns_str = format!(" returns ({})", returns_str) - } + Expression::Type(_, ty) => match ty { + Type::Address => "address".to_owned(), + Type::AddressPayable => "address payable".to_owned(), + Type::Payable => "payable".to_owned(), + Type::Bool => "bool".to_owned(), + Type::String => "string".to_owned(), + Type::Bytes(n) => format!("bytes{}", n), + Type::Rational => "rational".to_owned(), + Type::DynamicBytes => "bytes".to_owned(), + Type::Int(n) => format!("int{}", n), + Type::Uint(n) => format!("uint{}", n), + Type::Mapping(_, from, to) => { + format!("mapping({} => {})", from.as_code(), to.as_code()) + } + Type::Function { params, attributes, returns } => { + let params = params.as_code(); + let mut attributes = attributes.as_code(); + if !attributes.is_empty() { + attributes.insert(0, ' '); + } + let mut returns_str = String::new(); + if let Some((params, _attrs)) = returns { + returns_str = params.as_code(); + if !returns_str.is_empty() { + returns_str = format!(" returns ({})", returns_str) } - format!("function ({params}){attributes}{returns_str}") - }, + } + format!("function ({params}){attributes}{returns_str}") } - } + }, Expression::Variable(ident) => ident.name.to_owned(), - Expression::ArraySubscript(_, expr1, expr2) => format!("{}[{}]", expr1.as_code(), expr2.as_ref().map(|expr| expr.as_code()).unwrap_or_default()), - Expression::MemberAccess(_, expr, ident) => format!("{}.{}", ident.name, expr.as_code()), + Expression::ArraySubscript(_, expr1, expr2) => format!( + "{}[{}]", + expr1.as_code(), + expr2.as_ref().map(|expr| expr.as_code()).unwrap_or_default() + ), + Expression::MemberAccess(_, expr, ident) => { + format!("{}.{}", ident.name, expr.as_code()) + } item => { - println!("UNREACHABLE {:?}", item); // TODO: - unreachable!() + panic!("Attempted to format unsupported item: {item:?}") } - // ArraySlice( - // Loc, - // Box, - // Option>, - // Option>, - // ), - // FunctionCall(Loc, Box, Vec), - // NamedFunctionCall(Loc, Box, Vec), - // List(Loc, ParameterList), } } } @@ -205,8 +202,9 @@ impl AsCode for IdentifierPath { impl AsCode for StructDefinition { fn as_code(&self) -> String { + let name = &self.name.name; let fields = self.fields.iter().map(AsCode::as_code).join(";\n\t"); - format!("struct {} {{\n\t{};\n}}", self.name.name, fields) + format!("struct {name} {{\n\t{fields};\n}}") } } @@ -220,7 +218,7 @@ impl AsCode for EnumDefinition { fn as_code(&self) -> String { let name = &self.name.name; let values = self.values.iter().map(AsCode::as_code).join("\n\t"); - format!("enum {name}{{\n\t{values}\n}}") + format!("enum {name} {{\n\t{values}\n}}") } } diff --git a/doc/src/builder.rs b/doc/src/builder.rs index 5ba6463e44ee..0d659250f003 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -3,28 +3,35 @@ use eyre; use forge_fmt::Visitable; use itertools::Itertools; use rayon::prelude::*; -use solang_parser::{ - doccomment::DocCommentTag, - pt::{Base, Identifier, Parameter}, -}; +use solang_parser::pt::Base; use std::{ cmp::Ordering, collections::HashMap, - fmt::Write, fs, path::{Path, PathBuf}, }; use crate::{ - as_code::AsCode, config::DocConfig, format::DocFormat, - helpers::*, - macros::*, output::DocOutput, - parser::{DocElement, DocParser, DocPart}, + parser::{DocElement, DocItem, DocParser}, + writer::BufWriter, }; +#[derive(Debug)] +struct DocFile { + source: DocItem, + source_path: PathBuf, + target_path: PathBuf, +} + +impl DocFile { + fn new(source: DocItem, source_path: PathBuf, target_path: PathBuf) -> Self { + Self { source_path, source, target_path } + } +} + /// Build Solidity documentation for a project from natspec comments. /// The builder parses the source files using [DocParser], /// then formats and writes the elements as the output. @@ -33,7 +40,12 @@ pub struct DocBuilder { config: DocConfig, } +// TODO: consider using `tfio` impl DocBuilder { + const README: &'static str = "README.md"; + const SUMMARY: &'static str = "SUMMARY.md"; + const SOL_EXT: &'static str = "sol"; + /// Construct a new builder with default configuration. pub fn new() -> Self { DocBuilder { config: DocConfig::default() } @@ -44,15 +56,11 @@ impl DocBuilder { DocBuilder { config } } - /// Get the output directory for generated doc files. - pub fn out_dir_src(&self) -> PathBuf { - self.config.out_dir().join("src") - } - /// Parse the sources and build the documentation. pub fn build(self) -> eyre::Result<()> { + // Collect and parse source files let sources: Vec<_> = source_files_iter(&self.config.sources).collect(); - let docs = sources + let files = sources .par_iter() .enumerate() .map(|(i, path)| { @@ -67,475 +75,166 @@ impl DocBuilder { })?; let mut doc = DocParser::new(comments); source_unit.visit(&mut doc)?; - - Ok((path.clone(), doc.parts)) + Ok(doc + .items + .into_iter() + .map(|item| { + let relative_path = + path.strip_prefix(&self.config.root)?.join(item.filename()); + let target_path = self.config.out.join("src").join(relative_path); + Ok(DocFile::new(item, path.clone(), target_path)) + }) + .collect::>>()?) }) .collect::>>()?; - let docs = docs.into_iter().sorted_by(|(path1, _), (path2, _)| { - path1.display().to_string().cmp(&path2.display().to_string()) + // Flatten and sort the results + let files = files.into_iter().flatten().sorted_by(|file1, file2| { + file1.source_path.display().to_string().cmp(&file2.source_path.display().to_string()) }); - fs::create_dir_all(self.out_dir_src())?; - - let mut filenames = vec![]; - for (path, doc) in docs.as_ref() { - for part in doc.iter() { - match part.element { - DocElement::Contract(ref contract) => { - let mut doc_file = String::new(); - writeln_doc!(doc_file, DocOutput::H1(&contract.name.name))?; - - if !contract.base.is_empty() { - let bases = contract - .base - .iter() - .map(|base| { - Ok(self - .lookup_contract_base(docs.as_ref(), base)? - .unwrap_or(base.doc())) - }) - .collect::>>()?; - - writeln_doc!( - doc_file, - "{} {}\n", - DocOutput::Bold("Inherits:"), - bases.join(", ") - )?; - } - - writeln_doc!(doc_file, part.comments)?; - - let mut attributes = vec![]; - let mut funcs = vec![]; - let mut events = vec![]; - let mut errors = vec![]; - let mut structs = vec![]; - let mut enumerables = vec![]; - - for child in part.children.iter() { - // TODO: remove `clone`s - match &child.element { - DocElement::Function(func) => funcs.push((func, &child.comments)), - DocElement::Variable(var) => { - attributes.push((var, &child.comments)) - } - DocElement::Event(event) => events.push((event, &child.comments)), - DocElement::Error(error) => errors.push((error, &child.comments)), - DocElement::Struct(structure) => { - structs.push((structure, &child.comments)) - } - DocElement::Enum(enumerable) => { - enumerables.push((enumerable, &child.comments)) - } - _ => (), - } - } - - if !attributes.is_empty() { - writeln_doc!( - doc_file, - "{}", - self.format_section("Attributes", attributes)? - )?; - } - - if !funcs.is_empty() { - writeln_doc!(doc_file, DocOutput::H2("Functions"))?; - for (func, comments) in funcs { - writeln_doc!(doc_file, "{}\n", func)?; - writeln_doc!( - doc_file, - "{}\n", - filter_comments_without_tags( - &comments, - vec!["param", "return"] - ) - )?; - writeln_code!(doc_file, "{}", func)?; - - let params: Vec<_> = - func.params.iter().filter_map(|p| p.1.as_ref()).collect(); - let param_comments = filter_comments_by_tag(&comments, "param"); - if !params.is_empty() && !param_comments.is_empty() { - writeln_doc!( - doc_file, - "{}\n{}", - DocOutput::H3("Parameters"), - self.format_comment_table( - &["Name", "Type", "Description"], - ¶ms, - ¶m_comments - )? - )?; - } - - let returns: Vec<_> = - func.returns.iter().filter_map(|p| p.1.as_ref()).collect(); - let returns_comments = filter_comments_by_tag(&comments, "return"); - if !returns.is_empty() && !returns_comments.is_empty() { - writeln_doc!( - doc_file, - "{}\n{}", - DocOutput::H3("Returns"), - self.format_comment_table( - &["Name", "Type", "Description"], - &returns, - &returns_comments - )? - )?; - } - - writeln!(doc_file)?; - } - } - - if !events.is_empty() { - writeln_doc!(doc_file, "{}", self.format_section("Events", events)?)?; - } - - if !errors.is_empty() { - writeln_doc!(doc_file, "{}", self.format_section("Errors", errors)?)?; - } - - if !structs.is_empty() { - writeln_doc!(doc_file, "{}", self.format_section("Structs", structs)?)?; - } - - if !enumerables.is_empty() { - writeln_doc!( - doc_file, - "{}", - self.format_section("Enums", enumerables)? - )?; - } - - let new_path = path.strip_prefix(&self.config.root)?.to_owned(); - fs::create_dir_all(self.out_dir_src().join(&new_path))?; - let new_path = new_path.join(&format!("contract.{}.md", contract.name)); - - fs::write(self.out_dir_src().join(&new_path), doc_file)?; - filenames.push((contract.name.clone(), new_path)); - } - DocElement::Variable(ref attribute) => { - let mut doc_file = String::new(); - let mut variable_section = vec![]; - variable_section.push((attribute, &part.comments)); - writeln_doc!( - doc_file, - "{}", - self.format_section("Attribute", variable_section)? - )?; - - let new_path = path.strip_prefix(&self.config.root)?.to_owned(); - fs::create_dir_all(self.out_dir_src().join(&new_path))?; - let new_path = new_path.join(&format!("variable.{}.md", attribute.name)); - - fs::write(self.out_dir_src().join(&new_path), doc_file)?; - filenames.push((attribute.name.clone(), new_path)); - } - DocElement::Error(ref error) => { - let mut doc_file = String::new(); - let mut error_section = vec![]; - error_section.push((error, &part.comments)); - writeln_doc!(doc_file, "{}", self.format_section("Error", error_section)?)?; - - let new_path = path.strip_prefix(&self.config.root)?.to_owned(); - fs::create_dir_all(self.out_dir_src().join(&new_path))?; - let new_path = new_path.join(&format!("error.{}.md", error.name)); - - fs::write(self.out_dir_src().join(&new_path), doc_file)?; - filenames.push((error.name.clone(), new_path)); - } - DocElement::Event(ref event) => { - let mut doc_file = String::new(); - let mut event_section = vec![]; - event_section.push((event, &part.comments)); - writeln_doc!(doc_file, "{}", self.format_section("Event", event_section)?)?; - - let new_path = path.strip_prefix(&self.config.root)?.to_owned(); - fs::create_dir_all(self.out_dir_src().join(&new_path))?; - let new_path = new_path.join(&format!("event.{}.md", event.name)); - - fs::write(self.out_dir_src().join(&new_path), doc_file)?; - filenames.push((event.name.clone(), new_path)); - } - DocElement::Struct(ref structure) => { - let mut doc_file = String::new(); - let mut struct_section = vec![]; - struct_section.push((structure, &part.comments)); - writeln_doc!( - doc_file, - "{}", - self.format_section("Struct", struct_section)? - )?; - - let new_path = path.strip_prefix(&self.config.root)?.to_owned(); - fs::create_dir_all(self.out_dir_src().join(&new_path))?; - let new_path = new_path.join(&format!("struct.{}.md", structure.name)); - - fs::write(self.out_dir_src().join(&new_path), doc_file)?; - filenames.push((structure.name.clone(), new_path)); - } - DocElement::Enum(ref enumerable) => { - let mut doc_file = String::new(); - let mut enum_section = vec![]; - enum_section.push((enumerable, &part.comments)); - writeln_doc!(doc_file, "{}", self.format_section("Enum", enum_section)?)?; - - let new_path = path.strip_prefix(&self.config.root)?.to_owned(); - fs::create_dir_all(self.out_dir_src().join(&new_path))?; - let new_path = new_path.join(&format!("enum.{}.md", enumerable.name)); - - fs::write(self.out_dir_src().join(&new_path), doc_file)?; - filenames.push((enumerable.name.clone(), new_path)); - } - DocElement::Function(ref func) => { - let mut doc_file = String::new(); - writeln_doc!(doc_file, DocOutput::H1("Function"))?; - - writeln_doc!(doc_file, "{}\n", func)?; - writeln_doc!( - doc_file, - "{}\n", - filter_comments_without_tags(&part.comments, vec!["param", "return"]) - )?; - writeln_code!(doc_file, "{}", func)?; - - let params: Vec<_> = - func.params.iter().filter_map(|p| p.1.as_ref()).collect(); - let param_comments = filter_comments_by_tag(&part.comments, "param"); - if !params.is_empty() && !param_comments.is_empty() { - writeln_doc!( - doc_file, - "{}\n{}", - DocOutput::H3("Parameters"), - self.format_comment_table( - &["Name", "Type", "Description"], - ¶ms, - ¶m_comments - )? - )?; - } - - let returns: Vec<_> = - func.returns.iter().filter_map(|p| p.1.as_ref()).collect(); - let returns_comments = filter_comments_by_tag(&part.comments, "return"); - if !returns.is_empty() && !returns_comments.is_empty() { - writeln_doc!( - doc_file, - "{}\n{}", - DocOutput::H3("Returns"), - self.format_comment_table( - &["Name", "Type", "Description"], - &returns, - &returns_comments - )? - )?; - } - writeln!(doc_file)?; - let new_path = path.strip_prefix(&self.config.root)?.to_owned(); - fs::create_dir_all(self.out_dir_src().join(&new_path))?; - let func_name = - func.name.as_ref().map(|name| name.name.to_owned()).unwrap_or_default(); - let new_path = new_path.join(&format!("function.{}.md", func_name)); - - fs::write(self.out_dir_src().join(&new_path), doc_file)?; - // filenames.push((func.name.clone(), new_path)); - } - } - } - } - - self.write_book_config(filenames)?; + // Write mdbook related files + self.write_mdbook(files.collect::>())?; Ok(()) } - fn format_comment_table( - &self, - headers: &[&str], - params: &[&Parameter], - comments: &[&DocCommentTag], - ) -> eyre::Result { - let mut out = String::new(); - let separator = headers.iter().map(|h| "-".repeat(h.len())).join("|"); - - writeln!(out, "|{}|", headers.join("|"))?; - writeln!(out, "|{separator}|")?; - for param in params { - let param_name = param.name.as_ref().map(|n| n.name.to_owned()); - let description = param_name - .as_ref() - .map(|name| { - comments.iter().find_map(|comment| { - match comment.value.trim_start().split_once(' ') { - Some((tag_name, description)) if tag_name.trim().eq(name.as_str()) => { - Some(description.replace("\n", " ")) - } - _ => None, - } - }) - }) - .flatten() - .unwrap_or_default(); - let row = [ - param_name.unwrap_or_else(|| "".to_owned()), - param.ty.as_code(), - description, - ]; - writeln!(out, "|{}|", row.join("|"))?; - } - - Ok(out) - } - - fn format_section( - &self, - title: &str, - entries: Vec<(&T, &Vec)>, - ) -> eyre::Result { - let mut out = String::new(); - - writeln!(out, "{}", DocOutput::H2(title))?; - for (ev, comments) in entries { - writeln!(out, "{}\n{}\n", ev.doc(), comments.doc())?; - writeln_code!(out, "{}", ev)?; - writeln!(out)?; - } - - return Ok(out) - } - - fn write_book_config(&self, filenames: Vec<(Identifier, PathBuf)>) -> eyre::Result<()> { - let out_dir_src = self.out_dir_src(); + fn write_mdbook(&self, files: Vec) -> eyre::Result<()> { + let out_dir = self.config.out_dir(); + let out_dir_src = out_dir.join("src"); + fs::create_dir_all(&out_dir_src)?; + // Write readme content if any let readme_content = { - let src_readme = self.config.sources.join("README.md"); + let src_readme = self.config.sources.join(Self::README); if src_readme.exists() { fs::read_to_string(src_readme)? } else { String::new() } }; - let readme_path = out_dir_src.join("README.md"); + let readme_path = out_dir_src.join(Self::README); fs::write(&readme_path, readme_content)?; - let mut summary = String::new(); - writeln_doc!(summary, DocOutput::H1("Summary"))?; - writeln_doc!(summary, "- {}", DocOutput::Link("README", "README.md"))?; - - self.write_summary_section(&mut summary, 0, None, &filenames)?; - - fs::write(out_dir_src.join("SUMMARY.md"), summary.clone())?; + // Write summary and section readmes + let mut summary = BufWriter::default(); + summary.write_title("Summary")?; + summary.write_link_list_item("README", Self::README, 0)?; + // TODO: + self.write_summary_section(&mut summary, &files.iter().collect::>(), None, 0)?; + fs::write(out_dir_src.join(Self::SUMMARY), summary.finish())?; + // Create dir for static files let out_dir_static = out_dir_src.join("static"); fs::create_dir_all(&out_dir_static)?; + + // Write solidity syntax highlighting fs::write( out_dir_static.join("solidity.min.js"), include_str!("../static/solidity.min.js"), )?; + // Write book config let mut book: toml::Value = toml::from_str(include_str!("../static/book.toml"))?; let book_entry = book["book"].as_table_mut().unwrap(); book_entry.insert(String::from("title"), self.config.title.clone().into()); fs::write(self.config.out_dir().join("book.toml"), toml::to_string_pretty(&book)?)?; + // Write .gitignore + let gitignore = "book/"; + fs::write(self.config.out_dir().join(".gitignore"), gitignore)?; + + // Write doc files + for file in files { + let doc_content = file.source.doc()?; + fs::create_dir_all( + file.target_path.parent().ok_or(eyre::format_err!("empty target path; noop"))?, + )?; + fs::write(file.target_path, doc_content)?; + } + Ok(()) } fn write_summary_section( &self, - out: &mut String, - depth: usize, + summary: &mut BufWriter, + files: &[&DocFile], path: Option<&Path>, - entries: &[(Identifier, PathBuf)], + depth: usize, ) -> eyre::Result<()> { - if !entries.is_empty() { - if let Some(path) = path { - let title = path.iter().last().unwrap().to_string_lossy(); - let section_title = if depth == 1 { - DocOutput::H1(&title).doc() - } else { - format!( - "{}- {}", - " ".repeat((depth - 1) * 2), - DocOutput::Link( - &title, - &path.join("README.md").as_os_str().to_string_lossy() - ) - ) - }; - writeln_doc!(out, section_title)?; + if files.is_empty() { + return Ok(()) + } + + if let Some(path) = path { + let title = path.iter().last().unwrap().to_string_lossy(); + if depth == 1 { + summary.write_title(&title)?; + } else { + let summary_path = path.join(Self::README); + summary.write_link_list_item( + &title, + &summary_path.as_os_str().to_string_lossy(), + depth - 1, + )?; } + } - // group and sort entries - let mut grouped = HashMap::new(); - for entry in entries { - let key = entry.1.iter().take(depth + 1).collect::(); - grouped.entry(key).or_insert_with(Vec::new).push(entry.clone()); + // Group entries by path depth + let mut grouped = HashMap::new(); + for file in files { + let path = file.source_path.strip_prefix(&self.config.root)?; + let key = path.iter().take(depth + 1).collect::(); + grouped.entry(key).or_insert_with(Vec::new).push(file.clone()); + } + // Sort entries by path depth + let grouped = grouped.into_iter().sorted_by(|(lhs, _), (rhs, _)| { + let lhs_at_end = lhs.extension().map(|ext| ext.eq(Self::SOL_EXT)).unwrap_or_default(); + let rhs_at_end = rhs.extension().map(|ext| ext.eq(Self::SOL_EXT)).unwrap_or_default(); + if lhs_at_end == rhs_at_end { + lhs.cmp(rhs) + } else if lhs_at_end { + Ordering::Greater + } else { + Ordering::Less } - let grouped = grouped.iter().sorted_by(|(lhs, _), (rhs, _)| { - let lhs_at_end = lhs.extension().map(|ext| ext.eq("sol")).unwrap_or_default(); - let rhs_at_end = rhs.extension().map(|ext| ext.eq("sol")).unwrap_or_default(); - if lhs_at_end == rhs_at_end { - lhs.cmp(rhs) - } else if lhs_at_end { - Ordering::Greater - } else { - Ordering::Less - } - }); + }); - let indent = " ".repeat(2 * depth); - let mut readme = String::from("\n\n# Contents\n"); - for (path, entries) in grouped { - if path.extension().map(|ext| ext.eq("sol")).unwrap_or_default() { - for (src, filename) in entries { - writeln_doc!( - readme, - "- {}", - DocOutput::Link( - &src.name, - &Path::new("/").join(filename).display().to_string() - ) - )?; + let mut readme = BufWriter::default(); + readme.write_raw("\n\n# Contents\n")?; // TODO: + for (path, files) in grouped { + if path.extension().map(|ext| ext.eq(Self::SOL_EXT)).unwrap_or_default() { + for file in files { + let ident = file.source.element.ident(); - writeln_doc!( - out, - "{}- {}", - indent, - DocOutput::Link(&src.name, &filename.display().to_string()) - )?; - } - } else { - writeln_doc!( - readme, - "- {}", - DocOutput::Link( - &path.iter().last().unwrap().to_string_lossy(), - &Path::new("/").join(path).display().to_string() - ) - )?; - self.write_summary_section(out, depth + 1, Some(&path), &entries)?; + let readme_path = Path::new("/").join(&path).display().to_string(); + readme.write_link_list_item(&ident, &readme_path, 0)?; + + let summary_path = file.target_path.display().to_string(); + summary.write_link_list_item(&ident, &summary_path, depth)?; } + } else { + let name = path.iter().last().unwrap().to_string_lossy(); + let readme_path = Path::new("/").join(&path).display().to_string(); + readme.write_link_list_item(&name, &readme_path, 0)?; + self.write_summary_section(summary, &files, Some(&path), depth + 1)?; } - if !readme.is_empty() { - if let Some(path) = path { - fs::write( - self.config.out_dir().join("src").join(path).join("README.md"), - readme, - )?; - } + } + if !readme.is_empty() { + if let Some(path) = path { + let path = self.config.out_dir().join("src").join(path); + fs::create_dir_all(&path)?; + fs::write(path.join(Self::README), readme.finish())?; } } Ok(()) } + // TODO: fn lookup_contract_base<'a>( &self, - docs: &[(PathBuf, Vec)], + docs: &[(PathBuf, Vec)], base: &Base, ) -> eyre::Result> { for (base_path, base_doc) in docs { @@ -548,7 +247,7 @@ impl DocBuilder { .join(&format!("contract.{}.md", base_contract.name)), ); return Ok(Some( - DocOutput::Link(&base.doc(), &path.display().to_string()).doc(), + DocOutput::Link(&base.doc()?, &path.display().to_string()).doc()?, )) } } diff --git a/doc/src/format.rs b/doc/src/format.rs index be351dd8921d..385169968053 100644 --- a/doc/src/format.rs +++ b/doc/src/format.rs @@ -1,4 +1,9 @@ -use crate::output::DocOutput; +use crate::{ + helpers::{comments_by_tag, exclude_comment_tags}, + output::DocOutput, + parser::{DocElement, DocItem}, + writer::BufWriter, +}; use itertools::Itertools; use solang_parser::{ doccomment::DocCommentTag, @@ -8,93 +13,268 @@ use solang_parser::{ }, }; -/// TODO: -pub(crate) trait DocFormat { - fn doc(&self) -> String; -} +pub(crate) type DocResult = Result; -impl<'a> DocFormat for DocOutput<'a> { - fn doc(&self) -> String { - match self { - Self::H1(val) => format!("# {val}"), - Self::H2(val) => format!("## {val}"), - Self::H3(val) => format!("### {val}"), - Self::Bold(val) => format!("**{val}**"), - Self::Link(val, link) => format!("[{val}]({link})"), - Self::CodeBlock(lang, val) => format!("```{lang}\n{val}\n```"), - } - } +#[auto_impl::auto_impl(&)] +pub(crate) trait DocFormat { + fn doc(&self) -> DocResult; } impl DocFormat for String { - fn doc(&self) -> String { - self.to_owned() + fn doc(&self) -> DocResult { + Ok(self.to_owned()) } } impl DocFormat for DocCommentTag { - fn doc(&self) -> String { - self.value.to_owned() + fn doc(&self) -> DocResult { + Ok(self.value.to_owned()) } } impl DocFormat for Vec<&DocCommentTag> { - fn doc(&self) -> String { - self.iter().map(|c| DocCommentTag::doc(*c)).join("\n\n") + fn doc(&self) -> DocResult { + Ok(self.iter().map(|c| DocCommentTag::doc(*c)).collect::, _>>()?.join("\n\n")) } } impl DocFormat for Vec { - fn doc(&self) -> String { - self.iter().map(DocCommentTag::doc).join("\n\n") + fn doc(&self) -> DocResult { + Ok(self.iter().map(DocCommentTag::doc).collect::, _>>()?.join("\n\n")) } } +// TODO: remove? impl DocFormat for Base { - fn doc(&self) -> String { - self.name.identifiers.iter().map(|ident| ident.name.to_owned()).join(".") + fn doc(&self) -> DocResult { + Ok(self.name.identifiers.iter().map(|ident| ident.name.to_owned()).join(".")) } } impl DocFormat for Vec { - fn doc(&self) -> String { - self.iter().map(|base| base.doc()).join(", ") - } -} - -impl DocFormat for VariableDefinition { - fn doc(&self) -> String { - DocOutput::H3(&self.name.name).doc() + fn doc(&self) -> DocResult { + Ok(self.iter().map(|base| base.doc()).collect::, _>>()?.join(", ")) } } +// TODO: remove impl DocFormat for FunctionDefinition { - fn doc(&self) -> String { + fn doc(&self) -> DocResult { let name = self.name.as_ref().map_or(self.ty.to_string(), |n| n.name.to_owned()); DocOutput::H3(&name).doc() } } impl DocFormat for EventDefinition { - fn doc(&self) -> String { + fn doc(&self) -> DocResult { DocOutput::H3(&self.name.name).doc() } } impl DocFormat for ErrorDefinition { - fn doc(&self) -> String { + fn doc(&self) -> DocResult { DocOutput::H3(&self.name.name).doc() } } impl DocFormat for StructDefinition { - fn doc(&self) -> String { + fn doc(&self) -> DocResult { DocOutput::H3(&self.name.name).doc() } } impl DocFormat for EnumDefinition { - fn doc(&self) -> String { + fn doc(&self) -> DocResult { DocOutput::H3(&self.name.name).doc() } } + +impl DocFormat for VariableDefinition { + fn doc(&self) -> DocResult { + DocOutput::H3(&self.name.name).doc() + } +} + +impl DocFormat for DocItem { + fn doc(&self) -> DocResult { + let mut writer = BufWriter::default(); + + match &self.element { + DocElement::Contract(contract) => { + writer.write_title(&contract.name.name)?; + + if !contract.base.is_empty() { + // TODO: + // Ok(self.lookup_contract_base(docs.as_ref(), base)?.unwrap_or(base.doc())) + // TODO: should be a name & perform lookup + let bases = contract + .base + .iter() + .map(|base| base.doc()) + .collect::, _>>()?; + writer.write_bold("Inherits:")?; + writer.write_raw(&bases.join(", "))?; + writer.writeln()?; + } + + writer.write_raw(&self.comments.doc()?)?; + + if let Some(state_vars) = self.variables() { + writer.write_subtitle("State Variables")?; + state_vars + .into_iter() + .try_for_each(|(item, comments)| writer.write_section(item, comments))?; + } + + if let Some(funcs) = self.functions() { + writer.write_subtitle("Functions")?; + funcs.into_iter().try_for_each(|(func, comments)| { + // Write function name + let func_name = + func.name.as_ref().map_or(func.ty.to_string(), |n| n.name.to_owned()); + writer.write_heading(&func_name)?; + writer.writeln()?; + + // Write function docs + writer.write_raw( + &exclude_comment_tags(&comments, vec!["param", "return"]).doc()?, + )?; + + // Write function header + writer.write_code(func)?; + + // Write function parameter comments in a table + let params = + func.params.iter().filter_map(|p| p.1.as_ref()).collect::>(); + let param_comments = comments_by_tag(&comments, "param"); + if !params.is_empty() && !param_comments.is_empty() { + writer.write_heading("Parameters")?; + writer.writeln()?; + writer.write_param_table( + &["Name", "Type", "Description"], + ¶ms, + ¶m_comments, + )? + } + + // Write function parameter comments in a table + let returns = + func.returns.iter().filter_map(|p| p.1.as_ref()).collect::>(); + let returns_comments = comments_by_tag(&comments, "return"); + if !returns.is_empty() && !returns_comments.is_empty() { + writer.write_heading("Returns")?; + writer.writeln()?; + writer.write_param_table( + &["Name", "Type", "Description"], + &returns, + &returns_comments, + )?; + } + + writer.writeln()?; + + Ok::<(), std::fmt::Error>(()) + })?; + } + + if let Some(events) = self.events() { + writer.write_subtitle("Events")?; + events.into_iter().try_for_each(|(item, comments)| { + writer.write_heading(&item.name.name)?; + writer.write_section(item, comments) + })?; + } + + if let Some(errors) = self.errors() { + writer.write_subtitle("Errors")?; + errors.into_iter().try_for_each(|(item, comments)| { + writer.write_heading(&item.name.name)?; + writer.write_section(item, comments) + })?; + } + + if let Some(structs) = self.structs() { + writer.write_subtitle("Structs")?; + structs.into_iter().try_for_each(|(item, comments)| { + writer.write_heading(&item.name.name)?; + writer.write_section(item, comments) + })?; + } + + if let Some(enums) = self.enums() { + writer.write_subtitle("Enums")?; + enums.into_iter().try_for_each(|(item, comments)| { + writer.write_heading(&item.name.name)?; + writer.write_section(item, comments) + })?; + } + } + DocElement::Variable(var) => { + writer.write_title(&var.name.name)?; + writer.write_section(var, &self.comments)?; + } + DocElement::Event(event) => { + writer.write_title(&event.name.name)?; + writer.write_section(event, &self.comments)?; + } + DocElement::Error(error) => { + writer.write_title(&error.name.name)?; + writer.write_section(error, &self.comments)?; + } + DocElement::Struct(structure) => { + writer.write_title(&structure.name.name)?; + writer.write_section(structure, &self.comments)?; + } + DocElement::Enum(enumerable) => { + writer.write_title(&enumerable.name.name)?; + writer.write_section(enumerable, &self.comments)?; + } + DocElement::Function(func) => { + // TODO: cleanup + // Write function name + let func_name = + func.name.as_ref().map_or(func.ty.to_string(), |n| n.name.to_owned()); + writer.write_heading(&func_name)?; + writer.writeln()?; + + // Write function docs + writer.write_raw( + &exclude_comment_tags(&self.comments, vec!["param", "return"]).doc()?, + )?; + + // Write function header + writer.write_code(func)?; + + // Write function parameter comments in a table + let params = func.params.iter().filter_map(|p| p.1.as_ref()).collect::>(); + let param_comments = comments_by_tag(&self.comments, "param"); + if !params.is_empty() && !param_comments.is_empty() { + writer.write_heading("Parameters")?; + writer.writeln()?; + writer.write_param_table( + &["Name", "Type", "Description"], + ¶ms, + ¶m_comments, + )? + } + + // Write function parameter comments in a table + let returns = func.returns.iter().filter_map(|p| p.1.as_ref()).collect::>(); + let returns_comments = comments_by_tag(&self.comments, "return"); + if !returns.is_empty() && !returns_comments.is_empty() { + writer.write_heading("Returns")?; + writer.writeln()?; + writer.write_param_table( + &["Name", "Type", "Description"], + &returns, + &returns_comments, + )?; + } + + writer.writeln()?; + } + }; + + Ok(writer.finish()) + } +} diff --git a/doc/src/helpers.rs b/doc/src/helpers.rs index 8ae4ae75f5e6..1261a055097c 100644 --- a/doc/src/helpers.rs +++ b/doc/src/helpers.rs @@ -2,7 +2,7 @@ use solang_parser::doccomment::DocCommentTag; /// Filter a collection of comments and return /// only those that match a given tag -pub(crate) fn filter_comments_by_tag<'a>( +pub(crate) fn comments_by_tag<'a>( comments: &'a [DocCommentTag], tag: &str, ) -> Vec<&'a DocCommentTag> { @@ -11,7 +11,7 @@ pub(crate) fn filter_comments_by_tag<'a>( /// Filter a collection of comments and return /// only those that do not have provided tags -pub(crate) fn filter_comments_without_tags<'a>( +pub(crate) fn exclude_comment_tags<'a>( comments: &'a [DocCommentTag], tags: Vec<&str>, ) -> Vec<&'a DocCommentTag> { diff --git a/doc/src/lib.rs b/doc/src/lib.rs index cfa0b6fc55b6..8a7bc08182df 100644 --- a/doc/src/lib.rs +++ b/doc/src/lib.rs @@ -17,6 +17,6 @@ mod builder; mod config; mod format; mod helpers; -mod macros; mod output; mod parser; +mod writer; diff --git a/doc/src/macros.rs b/doc/src/macros.rs deleted file mode 100644 index 36485c67081a..000000000000 --- a/doc/src/macros.rs +++ /dev/null @@ -1,20 +0,0 @@ -macro_rules! writeln_doc { - ($dst:expr, $arg:expr) => { - writeln_doc!($dst, "{}", $arg) - }; - ($dst:expr, $format:literal, $($arg:expr),*) => { - writeln!($dst, "{}", format_args!($format, $($arg.doc(),)*)) - }; -} - -macro_rules! writeln_code { - ($dst:expr, $arg:expr) => { - writeln_code!($dst, "{}", $arg) - }; - ($dst:expr, $format:literal, $($arg:expr),*) => { - writeln!($dst, "{}", $crate::output::DocOutput::CodeBlock("solidity", &format!($format, $($arg.as_code(),)*))) - }; -} - -pub(crate) use writeln_code; -pub(crate) use writeln_doc; diff --git a/doc/src/output.rs b/doc/src/output.rs index b36450d05f26..df08fd7d8069 100644 --- a/doc/src/output.rs +++ b/doc/src/output.rs @@ -1,5 +1,6 @@ -use crate::format::DocFormat; +use crate::format::{DocFormat, DocResult}; +/// TODO: rename pub(crate) enum DocOutput<'a> { H1(&'a str), H2(&'a str), @@ -9,8 +10,22 @@ pub(crate) enum DocOutput<'a> { CodeBlock(&'a str, &'a str), } +impl<'a> DocFormat for DocOutput<'a> { + fn doc(&self) -> DocResult { + let doc = match self { + Self::H1(val) => format!("# {val}"), + Self::H2(val) => format!("## {val}"), + Self::H3(val) => format!("### {val}"), + Self::Bold(val) => format!("**{val}**"), + Self::Link(val, link) => format!("[{val}]({link})"), + Self::CodeBlock(lang, val) => format!("```{lang}\n{val}\n```"), + }; + Ok(doc) + } +} + impl<'a> std::fmt::Display for DocOutput<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("{}", self.doc())) + f.write_fmt(format_args!("{}", self.doc()?)) } } diff --git a/doc/src/parser.rs b/doc/src/parser.rs index 972f1e038a41..74b400044ede 100644 --- a/doc/src/parser.rs +++ b/doc/src/parser.rs @@ -12,11 +12,12 @@ use thiserror::Error; #[derive(Error, Debug)] pub(crate) enum DocParserError {} +// TODO: type Result = std::result::Result; #[derive(Debug)] pub(crate) struct DocParser { - pub(crate) parts: Vec, + pub(crate) items: Vec, comments: Vec, start_at: usize, context: DocContext, @@ -24,14 +25,54 @@ pub(crate) struct DocParser { #[derive(Debug, Default)] struct DocContext { - parent: Option, + parent: Option, } #[derive(Debug, PartialEq)] -pub(crate) struct DocPart { - pub(crate) comments: Vec, +pub(crate) struct DocItem { pub(crate) element: DocElement, - pub(crate) children: Vec, + pub(crate) comments: Vec, + pub(crate) children: Vec, +} + +macro_rules! filter_children_fn { + ($vis:vis fn $name:ident(&self, $variant:ident) -> $ret:ty) => { + $vis fn $name<'a>(&'a self) -> Option)>> { + let items = self.children.iter().filter_map(|item| match item.element { + DocElement::$variant(ref inner) => Some((inner, &item.comments)), + _ => None, + }); + let items = items.collect::>(); + if !items.is_empty() { + Some(items) + } else { + None + } + } + }; +} + +impl DocItem { + filter_children_fn!(pub(crate) fn variables(&self, Variable) -> VariableDefinition); + filter_children_fn!(pub(crate) fn functions(&self, Function) -> FunctionDefinition); + filter_children_fn!(pub(crate) fn events(&self, Event) -> EventDefinition); + filter_children_fn!(pub(crate) fn errors(&self, Error) -> ErrorDefinition); + filter_children_fn!(pub(crate) fn structs(&self, Struct) -> StructDefinition); + filter_children_fn!(pub(crate) fn enums(&self, Enum) -> EnumDefinition); + + pub(crate) fn filename(&self) -> String { + let prefix = match self.element { + DocElement::Contract(_) => "contract", + DocElement::Function(_) => "function", + DocElement::Variable(_) => "variable", + DocElement::Event(_) => "event", + DocElement::Error(_) => "error", + DocElement::Struct(_) => "struct", + DocElement::Enum(_) => "enum", + }; + let ident = self.element.ident(); + format!("{prefix}.{ident}.md") + } } #[derive(Debug, PartialEq)] @@ -45,16 +86,32 @@ pub(crate) enum DocElement { Enum(EnumDefinition), } +impl DocElement { + pub(crate) fn ident(&self) -> String { + match self { + DocElement::Contract(contract) => contract.name.name.to_owned(), + DocElement::Variable(var) => var.name.name.to_owned(), + DocElement::Event(event) => event.name.name.to_owned(), + DocElement::Error(error) => error.name.name.to_owned(), + DocElement::Struct(structure) => structure.name.name.to_owned(), + DocElement::Enum(enumerable) => enumerable.name.name.to_owned(), + DocElement::Function(func) => { + func.name.as_ref().map(|name| name.name.to_owned()).unwrap_or_default() // TODO: + } + } + } +} + impl DocParser { pub(crate) fn new(comments: Vec) -> Self { - DocParser { parts: vec![], comments, start_at: 0, context: Default::default() } + DocParser { items: vec![], comments, start_at: 0, context: Default::default() } } fn with_parent( &mut self, - mut parent: DocPart, + mut parent: DocItem, mut visit: impl FnMut(&mut Self) -> Result<()>, - ) -> Result { + ) -> Result { let curr = self.context.parent.take(); self.context.parent = Some(parent); visit(self)?; @@ -64,11 +121,11 @@ impl DocParser { } fn add_element_to_parent(&mut self, element: DocElement, loc: Loc) { - let child = DocPart { comments: self.parse_docs(loc.start()), element, children: vec![] }; + let child = DocItem { comments: self.parse_docs(loc.start()), element, children: vec![] }; if let Some(parent) = self.context.parent.as_mut() { parent.children.push(child); } else { - self.parts.push(child); + self.items.push(child); } self.start_at = loc.end(); } @@ -92,7 +149,7 @@ impl Visitor for DocParser { for source in source_unit.0.iter_mut() { match source { SourceUnitPart::ContractDefinition(def) => { - let contract = DocPart { + let contract = DocItem { element: DocElement::Contract(def.clone()), comments: self.parse_docs(def.loc.start()), children: vec![], @@ -104,7 +161,7 @@ impl Visitor for DocParser { })?; self.start_at = def.loc.end(); - self.parts.push(contract); + self.items.push(contract); } SourceUnitPart::FunctionDefinition(func) => self.visit_function(func)?, SourceUnitPart::EventDefinition(event) => self.visit_event(event)?, @@ -154,6 +211,8 @@ mod tests { use super::*; use std::{fs, path::PathBuf}; + // TODO: write tests w/ sol source code + #[test] fn parse_docs() { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) diff --git a/doc/src/writer.rs b/doc/src/writer.rs new file mode 100644 index 000000000000..dcde74240932 --- /dev/null +++ b/doc/src/writer.rs @@ -0,0 +1,129 @@ +use itertools::Itertools; +use solang_parser::{doccomment::DocCommentTag, pt::Parameter}; +use std::fmt::{self, Display, Write}; + +use crate::{as_code::AsCode, format::DocFormat, output::DocOutput}; + +/// TODO: comments +#[derive(Default)] +pub(crate) struct BufWriter { + buf: String, +} + +impl BufWriter { + pub(crate) fn is_empty(&self) -> bool { + self.buf.is_empty() + } + + pub(crate) fn write_raw(&mut self, content: T) -> fmt::Result { + write!(self.buf, "{content}") + } + + pub(crate) fn writeln(&mut self) -> fmt::Result { + writeln!(self.buf) + } + + pub(crate) fn write_title(&mut self, title: &str) -> fmt::Result { + writeln!(self.buf, "{}", DocOutput::H1(title)) + } + + pub(crate) fn write_subtitle(&mut self, subtitle: &str) -> fmt::Result { + writeln!(self.buf, "{}", DocOutput::H2(subtitle)) + } + + pub(crate) fn write_heading(&mut self, subtitle: &str) -> fmt::Result { + writeln!(self.buf, "{}", DocOutput::H3(subtitle)) + } + + pub(crate) fn write_bold(&mut self, text: &str) -> fmt::Result { + writeln!(self.buf, "{}", DocOutput::Bold(text)) + } + + pub(crate) fn write_list_item(&mut self, item: &str, depth: usize) -> fmt::Result { + let indent = " ".repeat(depth * 2); + writeln!(self.buf, "{indent}- {item}") + } + + pub(crate) fn write_link_list_item( + &mut self, + name: &str, + path: &str, + depth: usize, + ) -> fmt::Result { + let link = DocOutput::Link(name, path); + self.write_list_item(&link.doc()?, depth) + } + + pub(crate) fn write_code(&mut self, item: T) -> fmt::Result { + let code = item.as_code(); + let block = DocOutput::CodeBlock("solidity", &code); + writeln!(self.buf, "{block}") + } + + // TODO: revise + pub(crate) fn write_section( + &mut self, + item: T, + comments: &Vec, + ) -> fmt::Result { + // self.write_subtitle(subtitle)?; // TODO: h2/h3 or no title + // for (entry, comments) in entries { + // writeln!(self.buf, "{}\n{}\n", entry.doc(), comments.doc())?; + // // writeln_code!(self.buf, "{}", entry)?; + // self.write_code(entry)?; + // self.writeln()?; + // } + writeln!(self.buf, "{}\n{}\n", item.doc()?, comments.doc()?)?; + self.write_code(item)?; + self.writeln()?; + Ok(()) + } + + pub(crate) fn write_param_table( + &mut self, + headers: &[&str], + params: &[&Parameter], + comments: &[&DocCommentTag], + ) -> fmt::Result { + self.write_piped(&headers.join("|"))?; + + let separator = headers.iter().map(|h| "-".repeat(h.len())).join("|"); + self.write_piped(&separator)?; + + for param in params { + let param_name = param.name.as_ref().map(|n| n.name.to_owned()); + let description = param_name + .as_ref() + .map(|name| { + comments.iter().find_map(|comment| { + match comment.value.trim_start().split_once(' ') { + Some((tag_name, description)) if tag_name.trim().eq(name.as_str()) => { + Some(description.replace("\n", " ")) + } + _ => None, + } + }) + }) + .flatten() + .unwrap_or_default(); + let row = [ + param_name.unwrap_or_else(|| "".to_owned()), + param.ty.as_code(), + description, + ]; + self.write_piped(&row.join("|"))?; + } + + Ok(()) + } + + pub(crate) fn write_piped(&mut self, content: &str) -> fmt::Result { + self.write_raw("|")?; + self.write_raw(content)?; + self.write_raw("|") + } + + pub(crate) fn finish(self) -> String { + self.buf + } +} From ee2c80bc10e0f36582fcfb0708472493922189ba Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Fri, 6 Jan 2023 08:56:10 +0200 Subject: [PATCH 22/67] extract config --- cli/src/cmd/forge/doc.rs | 17 ++++++----- config/src/doc.rs | 20 +++++++++++++ config/src/lib.rs | 8 +++++- doc/src/as_code.rs | 12 ++++---- doc/src/builder.rs | 61 ++++++++++++++++++++-------------------- doc/src/config.rs | 38 ------------------------- doc/src/format.rs | 12 ++++---- doc/src/lib.rs | 2 -- doc/src/writer.rs | 15 +++------- 9 files changed, 83 insertions(+), 102 deletions(-) create mode 100644 config/src/doc.rs delete mode 100644 doc/src/config.rs diff --git a/cli/src/cmd/forge/doc.rs b/cli/src/cmd/forge/doc.rs index 163988d3dd8a..a04f6f201b42 100644 --- a/cli/src/cmd/forge/doc.rs +++ b/cli/src/cmd/forge/doc.rs @@ -1,6 +1,6 @@ use crate::cmd::Cmd; use clap::{Parser, ValueHint}; -use forge_doc::{DocBuilder, DocConfig}; +use forge_doc::DocBuilder; use foundry_config::{find_project_root_path, load_config_with_root}; use std::path::PathBuf; @@ -30,13 +30,16 @@ impl Cmd for DocArgs { type Output = (); fn run(self) -> eyre::Result { + let root = self.root.clone().unwrap_or(find_project_root_path()?); let config = load_config_with_root(self.root.clone()); - DocBuilder::from_config(DocConfig { - root: self.root.as_ref().unwrap_or(&find_project_root_path()?).to_path_buf(), + + let builder = DocBuilder { + root, sources: config.project_paths().sources, - out: self.out.clone().unwrap_or_else(|| PathBuf::from("docs")), - ..Default::default() - }) - .build() + out: self.out.clone().unwrap_or(config.doc.out.clone()), + title: config.doc.title.clone(), + }; + + builder.build() } } diff --git a/config/src/doc.rs b/config/src/doc.rs new file mode 100644 index 000000000000..0bd8dcddafe7 --- /dev/null +++ b/config/src/doc.rs @@ -0,0 +1,20 @@ +//! Configuration specific to the `forge doc` command and the `forge_doc` package + +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; + +/// Contains the config for parsing and rendering docs +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct DocConfig { + /// Doc output path. + pub out: PathBuf, + /// The documentation title. + pub title: String, +} + +impl Default for DocConfig { + fn default() -> Self { + Self { out: PathBuf::from("docs"), title: "".to_owned() } + } +} diff --git a/config/src/lib.rs b/config/src/lib.rs index 38f542035f77..ee4f998d7ff7 100644 --- a/config/src/lib.rs +++ b/config/src/lib.rs @@ -64,6 +64,9 @@ pub use crate::fs_permissions::FsPermissions; pub mod error; pub use error::SolidityErrorCode; +pub mod doc; +pub use doc::DocConfig; + mod warning; pub use warning::*; @@ -333,6 +336,8 @@ pub struct Config { pub build_info_path: Option, /// Configuration for `forge fmt` pub fmt: FormatterConfig, + /// Configuration for `forge doc` + pub doc: DocConfig, /// Configures the permissions of cheat codes that touch the file system. /// /// This includes what operations can be executed (read, write) @@ -384,7 +389,7 @@ impl Config { /// Standalone sections in the config which get integrated into the selected profile pub const STANDALONE_SECTIONS: &'static [&'static str] = - &["rpc_endpoints", "etherscan", "fmt", "fuzz", "invariant"]; + &["rpc_endpoints", "etherscan", "fmt", "doc", "fuzz", "invariant"]; /// File name of config toml file pub const FILE_NAME: &'static str = "foundry.toml"; @@ -1740,6 +1745,7 @@ impl Default for Config { build_info: false, build_info_path: None, fmt: Default::default(), + doc: Default::default(), __non_exhaustive: (), __warnings: vec![], } diff --git a/doc/src/as_code.rs b/doc/src/as_code.rs index 55b0be89ed41..9206f8c10c90 100644 --- a/doc/src/as_code.rs +++ b/doc/src/as_code.rs @@ -50,7 +50,7 @@ impl AsCode for FunctionDefinition { } let mut returns = self.returns.as_code(); if !returns.is_empty() { - returns = format!(" returns ({})", returns) + returns = format!(" returns ({returns})") } format!("{ty}{name}({params}){attributes}{returns}") } @@ -65,11 +65,11 @@ impl AsCode for Expression { Type::Payable => "payable".to_owned(), Type::Bool => "bool".to_owned(), Type::String => "string".to_owned(), - Type::Bytes(n) => format!("bytes{}", n), + Type::Bytes(n) => format!("bytes{n}"), Type::Rational => "rational".to_owned(), Type::DynamicBytes => "bytes".to_owned(), - Type::Int(n) => format!("int{}", n), - Type::Uint(n) => format!("uint{}", n), + Type::Int(n) => format!("int{n}"), + Type::Uint(n) => format!("uint{n}"), Type::Mapping(_, from, to) => { format!("mapping({} => {})", from.as_code(), to.as_code()) } @@ -83,7 +83,7 @@ impl AsCode for Expression { if let Some((params, _attrs)) = returns { returns_str = params.as_code(); if !returns_str.is_empty() { - returns_str = format!(" returns ({})", returns_str) + returns_str = format!(" returns ({returns_str})") } } format!("function ({params}){attributes}{returns_str}") @@ -224,6 +224,6 @@ impl AsCode for EnumDefinition { impl AsCode for Identifier { fn as_code(&self) -> String { - format!("{}", self.name) + self.name.to_string() } } diff --git a/doc/src/builder.rs b/doc/src/builder.rs index 0d659250f003..ce2c1423a843 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -1,5 +1,4 @@ use ethers_solc::utils::source_files_iter; -use eyre; use forge_fmt::Visitable; use itertools::Itertools; use rayon::prelude::*; @@ -12,7 +11,6 @@ use std::{ }; use crate::{ - config::DocConfig, format::DocFormat, output::DocOutput, parser::{DocElement, DocItem, DocParser}, @@ -37,7 +35,14 @@ impl DocFile { /// then formats and writes the elements as the output. #[derive(Debug)] pub struct DocBuilder { - config: DocConfig, + /// The project root + pub root: PathBuf, + /// Path to Solidity source files. + pub sources: PathBuf, + /// Output path. + pub out: PathBuf, + /// The documentation title. + pub title: String, } // TODO: consider using `tfio` @@ -46,25 +51,20 @@ impl DocBuilder { const SUMMARY: &'static str = "SUMMARY.md"; const SOL_EXT: &'static str = "sol"; - /// Construct a new builder with default configuration. - pub fn new() -> Self { - DocBuilder { config: DocConfig::default() } - } - - /// Construct a new builder with provided configuration - pub fn from_config(config: DocConfig) -> Self { - DocBuilder { config } + /// Get the output directory + pub fn out_dir(&self) -> PathBuf { + self.root.join(&self.out) } /// Parse the sources and build the documentation. pub fn build(self) -> eyre::Result<()> { // Collect and parse source files - let sources: Vec<_> = source_files_iter(&self.config.sources).collect(); + let sources: Vec<_> = source_files_iter(&self.sources).collect(); let files = sources .par_iter() .enumerate() .map(|(i, path)| { - let source = fs::read_to_string(&path)?; + let source = fs::read_to_string(path)?; let (mut source_unit, comments) = solang_parser::parse(&source, i).map_err(|diags| { eyre::eyre!( @@ -75,16 +75,14 @@ impl DocBuilder { })?; let mut doc = DocParser::new(comments); source_unit.visit(&mut doc)?; - Ok(doc - .items + doc.items .into_iter() .map(|item| { - let relative_path = - path.strip_prefix(&self.config.root)?.join(item.filename()); - let target_path = self.config.out.join("src").join(relative_path); + let relative_path = path.strip_prefix(&self.root)?.join(item.filename()); + let target_path = self.out.join("src").join(relative_path); Ok(DocFile::new(item, path.clone(), target_path)) }) - .collect::>>()?) + .collect::>>() }) .collect::>>()?; @@ -100,13 +98,13 @@ impl DocBuilder { } fn write_mdbook(&self, files: Vec) -> eyre::Result<()> { - let out_dir = self.config.out_dir(); + let out_dir = self.out_dir(); let out_dir_src = out_dir.join("src"); fs::create_dir_all(&out_dir_src)?; // Write readme content if any let readme_content = { - let src_readme = self.config.sources.join(Self::README); + let src_readme = self.sources.join(Self::README); if src_readme.exists() { fs::read_to_string(src_readme)? } else { @@ -137,12 +135,12 @@ impl DocBuilder { // Write book config let mut book: toml::Value = toml::from_str(include_str!("../static/book.toml"))?; let book_entry = book["book"].as_table_mut().unwrap(); - book_entry.insert(String::from("title"), self.config.title.clone().into()); - fs::write(self.config.out_dir().join("book.toml"), toml::to_string_pretty(&book)?)?; + book_entry.insert(String::from("title"), self.title.clone().into()); + fs::write(self.out_dir().join("book.toml"), toml::to_string_pretty(&book)?)?; // Write .gitignore let gitignore = "book/"; - fs::write(self.config.out_dir().join(".gitignore"), gitignore)?; + fs::write(self.out_dir().join(".gitignore"), gitignore)?; // Write doc files for file in files { @@ -175,7 +173,7 @@ impl DocBuilder { let summary_path = path.join(Self::README); summary.write_link_list_item( &title, - &summary_path.as_os_str().to_string_lossy(), + &summary_path.display().to_string(), depth - 1, )?; } @@ -184,9 +182,9 @@ impl DocBuilder { // Group entries by path depth let mut grouped = HashMap::new(); for file in files { - let path = file.source_path.strip_prefix(&self.config.root)?; + let path = file.source_path.strip_prefix(&self.root)?; let key = path.iter().take(depth + 1).collect::(); - grouped.entry(key).or_insert_with(Vec::new).push(file.clone()); + grouped.entry(key).or_insert_with(Vec::new).push(*file); } // Sort entries by path depth let grouped = grouped.into_iter().sorted_by(|(lhs, _), (rhs, _)| { @@ -211,7 +209,8 @@ impl DocBuilder { let readme_path = Path::new("/").join(&path).display().to_string(); readme.write_link_list_item(&ident, &readme_path, 0)?; - let summary_path = file.target_path.display().to_string(); + let summary_path = + file.target_path.strip_prefix("docs/src")?.display().to_string(); summary.write_link_list_item(&ident, &summary_path, depth)?; } } else { @@ -223,7 +222,7 @@ impl DocBuilder { } if !readme.is_empty() { if let Some(path) = path { - let path = self.config.out_dir().join("src").join(path); + let path = self.out_dir().join("src").join(path); fs::create_dir_all(&path)?; fs::write(path.join(Self::README), readme.finish())?; } @@ -243,8 +242,8 @@ impl DocBuilder { if base.name.identifiers.last().unwrap().name == base_contract.name.name { let path = PathBuf::from("/").join( base_path - .strip_prefix(&self.config.root)? - .join(&format!("contract.{}.md", base_contract.name)), + .strip_prefix(&self.root)? + .join(format!("contract.{}.md", base_contract.name)), ); return Ok(Some( DocOutput::Link(&base.doc()?, &path.display().to_string()).doc()?, diff --git a/doc/src/config.rs b/doc/src/config.rs deleted file mode 100644 index 5e64cbefd46d..000000000000 --- a/doc/src/config.rs +++ /dev/null @@ -1,38 +0,0 @@ -use std::path::{Path, PathBuf}; - -// TODO: move & merge w/ Figment -/// The doc builder configuration -#[derive(Debug)] -pub struct DocConfig { - /// The project root - pub root: PathBuf, - /// Path to Solidity source files. - pub sources: PathBuf, - /// Output path. - pub out: PathBuf, - /// The documentation title. - pub title: String, -} - -impl DocConfig { - /// Construct new documentation - pub fn new(root: &Path) -> Self { - DocConfig { root: root.to_owned(), ..Default::default() } - } - - /// Get the output directory - pub fn out_dir(&self) -> PathBuf { - self.root.join(&self.out) - } -} - -impl Default for DocConfig { - fn default() -> Self { - DocConfig { - root: PathBuf::new(), - sources: PathBuf::new(), - out: PathBuf::from("docs"), - title: "".to_owned(), - } - } -} diff --git a/doc/src/format.rs b/doc/src/format.rs index 385169968053..fc7f8c821456 100644 --- a/doc/src/format.rs +++ b/doc/src/format.rs @@ -113,11 +113,11 @@ impl DocFormat for DocItem { .map(|base| base.doc()) .collect::, _>>()?; writer.write_bold("Inherits:")?; - writer.write_raw(&bases.join(", "))?; + writer.write_raw(bases.join(", "))?; writer.writeln()?; } - writer.write_raw(&self.comments.doc()?)?; + writer.write_raw(self.comments.doc()?)?; if let Some(state_vars) = self.variables() { writer.write_subtitle("State Variables")?; @@ -137,7 +137,7 @@ impl DocFormat for DocItem { // Write function docs writer.write_raw( - &exclude_comment_tags(&comments, vec!["param", "return"]).doc()?, + exclude_comment_tags(comments, vec!["param", "return"]).doc()?, )?; // Write function header @@ -146,7 +146,7 @@ impl DocFormat for DocItem { // Write function parameter comments in a table let params = func.params.iter().filter_map(|p| p.1.as_ref()).collect::>(); - let param_comments = comments_by_tag(&comments, "param"); + let param_comments = comments_by_tag(comments, "param"); if !params.is_empty() && !param_comments.is_empty() { writer.write_heading("Parameters")?; writer.writeln()?; @@ -160,7 +160,7 @@ impl DocFormat for DocItem { // Write function parameter comments in a table let returns = func.returns.iter().filter_map(|p| p.1.as_ref()).collect::>(); - let returns_comments = comments_by_tag(&comments, "return"); + let returns_comments = comments_by_tag(comments, "return"); if !returns.is_empty() && !returns_comments.is_empty() { writer.write_heading("Returns")?; writer.writeln()?; @@ -239,7 +239,7 @@ impl DocFormat for DocItem { // Write function docs writer.write_raw( - &exclude_comment_tags(&self.comments, vec!["param", "return"]).doc()?, + exclude_comment_tags(&self.comments, vec!["param", "return"]).doc()?, )?; // Write function header diff --git a/doc/src/lib.rs b/doc/src/lib.rs index 8a7bc08182df..d242a3fb39d4 100644 --- a/doc/src/lib.rs +++ b/doc/src/lib.rs @@ -10,11 +10,9 @@ //! See [DocBuilder] pub use builder::DocBuilder; -pub use config::DocConfig; mod as_code; mod builder; -mod config; mod format; mod helpers; mod output; diff --git a/doc/src/writer.rs b/doc/src/writer.rs index dcde74240932..67e797908b32 100644 --- a/doc/src/writer.rs +++ b/doc/src/writer.rs @@ -66,14 +66,8 @@ impl BufWriter { item: T, comments: &Vec, ) -> fmt::Result { - // self.write_subtitle(subtitle)?; // TODO: h2/h3 or no title - // for (entry, comments) in entries { - // writeln!(self.buf, "{}\n{}\n", entry.doc(), comments.doc())?; - // // writeln_code!(self.buf, "{}", entry)?; - // self.write_code(entry)?; - // self.writeln()?; - // } - writeln!(self.buf, "{}\n{}\n", item.doc()?, comments.doc()?)?; + self.write_raw(&comments.doc()?)?; + self.writeln()?; self.write_code(item)?; self.writeln()?; Ok(()) @@ -94,17 +88,16 @@ impl BufWriter { let param_name = param.name.as_ref().map(|n| n.name.to_owned()); let description = param_name .as_ref() - .map(|name| { + .and_then(|name| { comments.iter().find_map(|comment| { match comment.value.trim_start().split_once(' ') { Some((tag_name, description)) if tag_name.trim().eq(name.as_str()) => { - Some(description.replace("\n", " ")) + Some(description.replace('\n', " ")) } _ => None, } }) }) - .flatten() .unwrap_or_default(); let row = [ param_name.unwrap_or_else(|| "".to_owned()), From 750b09b047f0c3bbd6c75dae2d7b3979a0ae78ea Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Fri, 6 Jan 2023 09:47:05 +0200 Subject: [PATCH 23/67] refactor parser --- doc/src/builder.rs | 18 ++-- doc/src/format.rs | 20 ++-- doc/src/lib.rs | 5 +- doc/src/parser.rs | 227 ---------------------------------------- doc/src/parser/error.rs | 16 +++ doc/src/parser/item.rs | 99 ++++++++++++++++++ doc/src/parser/mod.rs | 174 ++++++++++++++++++++++++++++++ 7 files changed, 311 insertions(+), 248 deletions(-) delete mode 100644 doc/src/parser.rs create mode 100644 doc/src/parser/error.rs create mode 100644 doc/src/parser/item.rs create mode 100644 doc/src/parser/mod.rs diff --git a/doc/src/builder.rs b/doc/src/builder.rs index ce2c1423a843..d34ed51f6e2b 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -13,25 +13,25 @@ use std::{ use crate::{ format::DocFormat, output::DocOutput, - parser::{DocElement, DocItem, DocParser}, + parser::{ParseItem, ParseSource, Parser}, writer::BufWriter, }; #[derive(Debug)] struct DocFile { - source: DocItem, + source: ParseItem, source_path: PathBuf, target_path: PathBuf, } impl DocFile { - fn new(source: DocItem, source_path: PathBuf, target_path: PathBuf) -> Self { + fn new(source: ParseItem, source_path: PathBuf, target_path: PathBuf) -> Self { Self { source_path, source, target_path } } } /// Build Solidity documentation for a project from natspec comments. -/// The builder parses the source files using [DocParser], +/// The builder parses the source files using [Parser], /// then formats and writes the elements as the output. #[derive(Debug)] pub struct DocBuilder { @@ -73,9 +73,9 @@ impl DocBuilder { diags ) })?; - let mut doc = DocParser::new(comments); + let mut doc = Parser::new(comments); source_unit.visit(&mut doc)?; - doc.items + doc.items() .into_iter() .map(|item| { let relative_path = path.strip_prefix(&self.root)?.join(item.filename()); @@ -204,7 +204,7 @@ impl DocBuilder { for (path, files) in grouped { if path.extension().map(|ext| ext.eq(Self::SOL_EXT)).unwrap_or_default() { for file in files { - let ident = file.source.element.ident(); + let ident = file.source.source.ident(); let readme_path = Path::new("/").join(&path).display().to_string(); readme.write_link_list_item(&ident, &readme_path, 0)?; @@ -233,12 +233,12 @@ impl DocBuilder { // TODO: fn lookup_contract_base<'a>( &self, - docs: &[(PathBuf, Vec)], + docs: &[(PathBuf, Vec)], base: &Base, ) -> eyre::Result> { for (base_path, base_doc) in docs { for base_part in base_doc.iter() { - if let DocElement::Contract(base_contract) = &base_part.element { + if let ParseSource::Contract(base_contract) = &base_part.source { if base.name.identifiers.last().unwrap().name == base_contract.name.name { let path = PathBuf::from("/").join( base_path diff --git a/doc/src/format.rs b/doc/src/format.rs index fc7f8c821456..2429ea63790a 100644 --- a/doc/src/format.rs +++ b/doc/src/format.rs @@ -1,7 +1,7 @@ use crate::{ helpers::{comments_by_tag, exclude_comment_tags}, output::DocOutput, - parser::{DocElement, DocItem}, + parser::{ParseItem, ParseSource}, writer::BufWriter, }; use itertools::Itertools; @@ -95,12 +95,12 @@ impl DocFormat for VariableDefinition { } } -impl DocFormat for DocItem { +impl DocFormat for ParseItem { fn doc(&self) -> DocResult { let mut writer = BufWriter::default(); - match &self.element { - DocElement::Contract(contract) => { + match &self.source { + ParseSource::Contract(contract) => { writer.write_title(&contract.name.name)?; if !contract.base.is_empty() { @@ -209,27 +209,27 @@ impl DocFormat for DocItem { })?; } } - DocElement::Variable(var) => { + ParseSource::Variable(var) => { writer.write_title(&var.name.name)?; writer.write_section(var, &self.comments)?; } - DocElement::Event(event) => { + ParseSource::Event(event) => { writer.write_title(&event.name.name)?; writer.write_section(event, &self.comments)?; } - DocElement::Error(error) => { + ParseSource::Error(error) => { writer.write_title(&error.name.name)?; writer.write_section(error, &self.comments)?; } - DocElement::Struct(structure) => { + ParseSource::Struct(structure) => { writer.write_title(&structure.name.name)?; writer.write_section(structure, &self.comments)?; } - DocElement::Enum(enumerable) => { + ParseSource::Enum(enumerable) => { writer.write_title(&enumerable.name.name)?; writer.write_section(enumerable, &self.comments)?; } - DocElement::Function(func) => { + ParseSource::Function(func) => { // TODO: cleanup // Write function name let func_name = diff --git a/doc/src/lib.rs b/doc/src/lib.rs index d242a3fb39d4..a9ecf06a2a87 100644 --- a/doc/src/lib.rs +++ b/doc/src/lib.rs @@ -9,8 +9,6 @@ //! //! See [DocBuilder] -pub use builder::DocBuilder; - mod as_code; mod builder; mod format; @@ -18,3 +16,6 @@ mod helpers; mod output; mod parser; mod writer; + +pub use builder::DocBuilder; +pub use parser::{error, ParseItem, ParseSource, Parser}; diff --git a/doc/src/parser.rs b/doc/src/parser.rs deleted file mode 100644 index 74b400044ede..000000000000 --- a/doc/src/parser.rs +++ /dev/null @@ -1,227 +0,0 @@ -use forge_fmt::{Visitable, Visitor}; -use solang_parser::{ - doccomment::{parse_doccomments, DocComment, DocCommentTag}, - pt::{ - Comment, ContractDefinition, EnumDefinition, ErrorDefinition, EventDefinition, - FunctionDefinition, Loc, SourceUnit, SourceUnitPart, StructDefinition, VariableDefinition, - }, -}; -use thiserror::Error; - -/// The parser error -#[derive(Error, Debug)] -pub(crate) enum DocParserError {} - -// TODO: -type Result = std::result::Result; - -#[derive(Debug)] -pub(crate) struct DocParser { - pub(crate) items: Vec, - comments: Vec, - start_at: usize, - context: DocContext, -} - -#[derive(Debug, Default)] -struct DocContext { - parent: Option, -} - -#[derive(Debug, PartialEq)] -pub(crate) struct DocItem { - pub(crate) element: DocElement, - pub(crate) comments: Vec, - pub(crate) children: Vec, -} - -macro_rules! filter_children_fn { - ($vis:vis fn $name:ident(&self, $variant:ident) -> $ret:ty) => { - $vis fn $name<'a>(&'a self) -> Option)>> { - let items = self.children.iter().filter_map(|item| match item.element { - DocElement::$variant(ref inner) => Some((inner, &item.comments)), - _ => None, - }); - let items = items.collect::>(); - if !items.is_empty() { - Some(items) - } else { - None - } - } - }; -} - -impl DocItem { - filter_children_fn!(pub(crate) fn variables(&self, Variable) -> VariableDefinition); - filter_children_fn!(pub(crate) fn functions(&self, Function) -> FunctionDefinition); - filter_children_fn!(pub(crate) fn events(&self, Event) -> EventDefinition); - filter_children_fn!(pub(crate) fn errors(&self, Error) -> ErrorDefinition); - filter_children_fn!(pub(crate) fn structs(&self, Struct) -> StructDefinition); - filter_children_fn!(pub(crate) fn enums(&self, Enum) -> EnumDefinition); - - pub(crate) fn filename(&self) -> String { - let prefix = match self.element { - DocElement::Contract(_) => "contract", - DocElement::Function(_) => "function", - DocElement::Variable(_) => "variable", - DocElement::Event(_) => "event", - DocElement::Error(_) => "error", - DocElement::Struct(_) => "struct", - DocElement::Enum(_) => "enum", - }; - let ident = self.element.ident(); - format!("{prefix}.{ident}.md") - } -} - -#[derive(Debug, PartialEq)] -pub(crate) enum DocElement { - Contract(Box), - Function(FunctionDefinition), - Variable(VariableDefinition), - Event(EventDefinition), - Error(ErrorDefinition), - Struct(StructDefinition), - Enum(EnumDefinition), -} - -impl DocElement { - pub(crate) fn ident(&self) -> String { - match self { - DocElement::Contract(contract) => contract.name.name.to_owned(), - DocElement::Variable(var) => var.name.name.to_owned(), - DocElement::Event(event) => event.name.name.to_owned(), - DocElement::Error(error) => error.name.name.to_owned(), - DocElement::Struct(structure) => structure.name.name.to_owned(), - DocElement::Enum(enumerable) => enumerable.name.name.to_owned(), - DocElement::Function(func) => { - func.name.as_ref().map(|name| name.name.to_owned()).unwrap_or_default() // TODO: - } - } - } -} - -impl DocParser { - pub(crate) fn new(comments: Vec) -> Self { - DocParser { items: vec![], comments, start_at: 0, context: Default::default() } - } - - fn with_parent( - &mut self, - mut parent: DocItem, - mut visit: impl FnMut(&mut Self) -> Result<()>, - ) -> Result { - let curr = self.context.parent.take(); - self.context.parent = Some(parent); - visit(self)?; - parent = self.context.parent.take().unwrap(); - self.context.parent = curr; - Ok(parent) - } - - fn add_element_to_parent(&mut self, element: DocElement, loc: Loc) { - let child = DocItem { comments: self.parse_docs(loc.start()), element, children: vec![] }; - if let Some(parent) = self.context.parent.as_mut() { - parent.children.push(child); - } else { - self.items.push(child); - } - self.start_at = loc.end(); - } - - fn parse_docs(&mut self, end: usize) -> Vec { - let mut res = vec![]; - for comment in parse_doccomments(&self.comments, self.start_at, end) { - match comment { - DocComment::Line { comment } => res.push(comment), - DocComment::Block { comments } => res.extend(comments.into_iter()), - } - } - res - } -} - -impl Visitor for DocParser { - type Error = DocParserError; - - fn visit_source_unit(&mut self, source_unit: &mut SourceUnit) -> Result<()> { - for source in source_unit.0.iter_mut() { - match source { - SourceUnitPart::ContractDefinition(def) => { - let contract = DocItem { - element: DocElement::Contract(def.clone()), - comments: self.parse_docs(def.loc.start()), - children: vec![], - }; - self.start_at = def.loc.start(); - let contract = self.with_parent(contract, |doc| { - def.parts.iter_mut().map(|d| d.visit(doc)).collect::>>()?; - Ok(()) - })?; - - self.start_at = def.loc.end(); - self.items.push(contract); - } - SourceUnitPart::FunctionDefinition(func) => self.visit_function(func)?, - SourceUnitPart::EventDefinition(event) => self.visit_event(event)?, - SourceUnitPart::ErrorDefinition(error) => self.visit_error(error)?, - SourceUnitPart::StructDefinition(structure) => self.visit_struct(structure)?, - SourceUnitPart::EnumDefinition(enumerable) => self.visit_enum(enumerable)?, - _ => {} - }; - } - - Ok(()) - } - - fn visit_function(&mut self, func: &mut FunctionDefinition) -> Result<()> { - self.add_element_to_parent(DocElement::Function(func.clone()), func.loc); - Ok(()) - } - - fn visit_var_definition(&mut self, var: &mut VariableDefinition) -> Result<()> { - self.add_element_to_parent(DocElement::Variable(var.clone()), var.loc); - Ok(()) - } - - fn visit_event(&mut self, event: &mut EventDefinition) -> Result<()> { - self.add_element_to_parent(DocElement::Event(event.clone()), event.loc); - Ok(()) - } - - fn visit_error(&mut self, error: &mut ErrorDefinition) -> Result<()> { - self.add_element_to_parent(DocElement::Error(error.clone()), error.loc); - Ok(()) - } - - fn visit_struct(&mut self, structure: &mut StructDefinition) -> Result<()> { - self.add_element_to_parent(DocElement::Struct(structure.clone()), structure.loc); - Ok(()) - } - - fn visit_enum(&mut self, enumerable: &mut EnumDefinition) -> Result<()> { - self.add_element_to_parent(DocElement::Enum(enumerable.clone()), enumerable.loc); - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::{fs, path::PathBuf}; - - // TODO: write tests w/ sol source code - - #[test] - fn parse_docs() { - let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("testdata") - .join("oz-governance") - .join("Governor.sol"); - let target = fs::read_to_string(path).unwrap(); - let (mut source_pt, source_comments) = solang_parser::parse(&target, 1).unwrap(); - let mut d = DocParser::new(source_comments); - assert!(source_pt.visit(&mut d).is_ok()); - } -} diff --git a/doc/src/parser/error.rs b/doc/src/parser/error.rs new file mode 100644 index 000000000000..fe9f09c854fa --- /dev/null +++ b/doc/src/parser/error.rs @@ -0,0 +1,16 @@ +use std::fmt::{Display, Formatter}; + +use thiserror::Error; + +/// The parser error +#[derive(Error, Debug)] +pub struct NoopError; + +impl Display for NoopError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str("noop;") + } +} + +/// A result with default noop error +pub type ParserResult = std::result::Result; diff --git a/doc/src/parser/item.rs b/doc/src/parser/item.rs new file mode 100644 index 000000000000..bde9b0464c8d --- /dev/null +++ b/doc/src/parser/item.rs @@ -0,0 +1,99 @@ +use solang_parser::{ + doccomment::DocCommentTag, + pt::{ + ContractDefinition, EnumDefinition, ErrorDefinition, EventDefinition, FunctionDefinition, + StructDefinition, VariableDefinition, + }, +}; + +/// The parsed item. +#[derive(Debug, PartialEq)] +pub struct ParseItem { + /// The parse tree source. + pub source: ParseSource, + /// Item comments. + pub comments: Vec, + /// Children items. + pub children: Vec, +} + +/// Filters [ParseItem]'s children and returns the source pt token of the children +/// matching the target variant as well as its comments. +/// Returns [Option::None] if no children matching the variant are found. +macro_rules! filter_children_fn { + ($vis:vis fn $name:ident(&self, $variant:ident) -> $ret:ty) => { + /// Filter children items for [ParseSource::$variant] variants. + $vis fn $name<'a>(&'a self) -> Option)>> { + let items = self.children.iter().filter_map(|item| match item.source { + ParseSource::$variant(ref inner) => Some((inner, &item.comments)), + _ => None, + }); + let items = items.collect::>(); + if !items.is_empty() { + Some(items) + } else { + None + } + } + }; +} + +impl ParseItem { + filter_children_fn!(pub fn variables(&self, Variable) -> VariableDefinition); + filter_children_fn!(pub fn functions(&self, Function) -> FunctionDefinition); + filter_children_fn!(pub fn events(&self, Event) -> EventDefinition); + filter_children_fn!(pub fn errors(&self, Error) -> ErrorDefinition); + filter_children_fn!(pub fn structs(&self, Struct) -> StructDefinition); + filter_children_fn!(pub fn enums(&self, Enum) -> EnumDefinition); + + /// Format the item's filename. + pub fn filename(&self) -> String { + let prefix = match self.source { + ParseSource::Contract(_) => "contract", + ParseSource::Function(_) => "function", + ParseSource::Variable(_) => "variable", + ParseSource::Event(_) => "event", + ParseSource::Error(_) => "error", + ParseSource::Struct(_) => "struct", + ParseSource::Enum(_) => "enum", + }; + let ident = self.source.ident(); + format!("{prefix}.{ident}.md") + } +} + +/// A wrapper type around pt token. +#[derive(Debug, PartialEq)] +pub enum ParseSource { + /// Source contract definition. + Contract(Box), + /// Source function definition. + Function(FunctionDefinition), + /// Source variable definition. + Variable(VariableDefinition), + /// Source event definition. + Event(EventDefinition), + /// Source error definition. + Error(ErrorDefinition), + /// Source struct definition. + Struct(StructDefinition), + /// Source enum definition. + Enum(EnumDefinition), +} + +impl ParseSource { + /// Get the identity of the source + pub fn ident(&self) -> String { + match self { + ParseSource::Contract(contract) => contract.name.name.to_owned(), + ParseSource::Variable(var) => var.name.name.to_owned(), + ParseSource::Event(event) => event.name.name.to_owned(), + ParseSource::Error(error) => error.name.name.to_owned(), + ParseSource::Struct(structure) => structure.name.name.to_owned(), + ParseSource::Enum(enumerable) => enumerable.name.name.to_owned(), + ParseSource::Function(func) => { + func.name.as_ref().map_or(func.ty.to_string(), |n| n.name.to_owned()) + } + } + } +} diff --git a/doc/src/parser/mod.rs b/doc/src/parser/mod.rs new file mode 100644 index 000000000000..918eb47f725c --- /dev/null +++ b/doc/src/parser/mod.rs @@ -0,0 +1,174 @@ +//! The parser module. + +use forge_fmt::{Visitable, Visitor}; +use solang_parser::{ + doccomment::{parse_doccomments, DocComment, DocCommentTag}, + pt::{ + Comment, EnumDefinition, ErrorDefinition, EventDefinition, FunctionDefinition, Loc, + SourceUnit, SourceUnitPart, StructDefinition, VariableDefinition, + }, +}; + +/// Parser error. +pub mod error; +use error::{NoopError, ParserResult}; + +mod item; +pub use item::{ParseItem, ParseSource}; + +/// The documentation parser. This type implements a [Visitor] trait. While walking the parse tree, +/// [Parser] will collect relevant source items and corresponding doc comments. The resulting +/// [ParseItem]s can be accessed by calling [Parser::items]. +#[derive(Debug, Default)] +pub struct Parser { + items: Vec, + comments: Vec, + start_at: usize, + context: ParserContext, +} + +/// [Parser]'s context. Holds information about current parent that's being visited. +#[derive(Debug, Default)] +struct ParserContext { + parent: Option, +} + +impl Parser { + /// Create a new instance of [Parser]. + pub fn new(comments: Vec) -> Self { + Parser { comments, ..Default::default() } + } + + /// Return the parsed items. Consumes the parser. + pub fn items(self) -> Vec { + self.items + } + + /// Visit the children elements with parent context. + /// This function memoizes the previous parent, sets the context + /// to a new one and invokes a visit function. The context will be reset + /// to the previous parent at the end of the function. + fn with_parent( + &mut self, + mut parent: ParseItem, + mut visit: impl FnMut(&mut Self) -> ParserResult<()>, + ) -> ParserResult { + let curr = self.context.parent.take(); + self.context.parent = Some(parent); + visit(self)?; + parent = self.context.parent.take().unwrap(); + self.context.parent = curr; + Ok(parent) + } + + /// Adds a child element to the parent parsed item. + /// Moves the doc comment pointer to the end location of the child element. + fn add_element_to_parent(&mut self, source: ParseSource, loc: Loc) { + let child = ParseItem { source, comments: self.parse_docs(loc.start()), children: vec![] }; + if let Some(parent) = self.context.parent.as_mut() { + parent.children.push(child); + } else { + self.items.push(child); + } + self.start_at = loc.end(); + } + + /// Parse the doc comments from the current start location. + fn parse_docs(&mut self, end: usize) -> Vec { + let mut res = vec![]; + for comment in parse_doccomments(&self.comments, self.start_at, end) { + match comment { + DocComment::Line { comment } => res.push(comment), + DocComment::Block { comments } => res.extend(comments.into_iter()), + } + } + res + } +} + +impl Visitor for Parser { + type Error = NoopError; + + fn visit_source_unit(&mut self, source_unit: &mut SourceUnit) -> ParserResult<()> { + for source in source_unit.0.iter_mut() { + match source { + SourceUnitPart::ContractDefinition(def) => { + let contract = ParseItem { + source: ParseSource::Contract(def.clone()), + comments: self.parse_docs(def.loc.start()), + children: vec![], + }; + self.start_at = def.loc.start(); + let contract = self.with_parent(contract, |doc| { + def.parts + .iter_mut() + .map(|d| d.visit(doc)) + .collect::>>()?; + Ok(()) + })?; + + self.start_at = def.loc.end(); + self.items.push(contract); + } + SourceUnitPart::FunctionDefinition(func) => self.visit_function(func)?, + SourceUnitPart::EventDefinition(event) => self.visit_event(event)?, + SourceUnitPart::ErrorDefinition(error) => self.visit_error(error)?, + SourceUnitPart::StructDefinition(structure) => self.visit_struct(structure)?, + SourceUnitPart::EnumDefinition(enumerable) => self.visit_enum(enumerable)?, + _ => {} + }; + } + + Ok(()) + } + + fn visit_function(&mut self, func: &mut FunctionDefinition) -> ParserResult<()> { + self.add_element_to_parent(ParseSource::Function(func.clone()), func.loc); + Ok(()) + } + + fn visit_var_definition(&mut self, var: &mut VariableDefinition) -> ParserResult<()> { + self.add_element_to_parent(ParseSource::Variable(var.clone()), var.loc); + Ok(()) + } + + fn visit_event(&mut self, event: &mut EventDefinition) -> ParserResult<()> { + self.add_element_to_parent(ParseSource::Event(event.clone()), event.loc); + Ok(()) + } + + fn visit_error(&mut self, error: &mut ErrorDefinition) -> ParserResult<()> { + self.add_element_to_parent(ParseSource::Error(error.clone()), error.loc); + Ok(()) + } + + fn visit_struct(&mut self, structure: &mut StructDefinition) -> ParserResult<()> { + self.add_element_to_parent(ParseSource::Struct(structure.clone()), structure.loc); + Ok(()) + } + + fn visit_enum(&mut self, enumerable: &mut EnumDefinition) -> ParserResult<()> { + self.add_element_to_parent(ParseSource::Enum(enumerable.clone()), enumerable.loc); + Ok(()) + } +} + +// #[cfg(test)] +// mod tests { +// use super::*; +// use std::{fs, path::PathBuf}; + +// // TODO: write tests w/ sol source code + +// #[test] +// fn parse_docs() { +// let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) +// .join("testdata") +// .join("oz-governance") +// .join("Governor.sol"); +// let target = fs::read_to_string(path).unwrap(); +// let (mut source_pt, source_comments) = solang_parser::parse(&target, 1).unwrap(); +// let mut d = DocParser::new(source_comments); +// assert!(source_pt.visit(&mut d).is_ok()); +// } +// } From 3aff142f70289cac8267760647e1c9f37b8fbb91 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Fri, 6 Jan 2023 11:22:24 +0200 Subject: [PATCH 24/67] refactor format traits and add parser tests --- Cargo.lock | 7 + doc/Cargo.toml | 3 + doc/src/builder.rs | 7 +- doc/src/{ => format}/as_code.rs | 7 +- doc/src/{format.rs => format/as_doc.rs} | 103 ++-- doc/src/format/mod.rs | 7 + doc/src/lib.rs | 7 +- doc/src/output.rs | 8 +- doc/src/parser/item.rs | 23 +- doc/src/parser/mod.rs | 144 +++++- doc/src/writer.rs | 11 +- doc/testdata/oz-governance/Governor.sol | 596 ------------------------ 12 files changed, 206 insertions(+), 717 deletions(-) rename doc/src/{ => format}/as_code.rs (97%) rename doc/src/{format.rs => format/as_doc.rs} (79%) create mode 100644 doc/src/format/mod.rs delete mode 100644 doc/testdata/oz-governance/Governor.sol diff --git a/Cargo.lock b/Cargo.lock index 12316f04445d..cd35ef1022f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -224,6 +224,12 @@ dependencies = [ "term", ] +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + [[package]] name = "async-priority-channel" version = "0.1.0" @@ -2325,6 +2331,7 @@ dependencies = [ name = "forge-doc" version = "0.1.0" dependencies = [ + "assert_matches", "auto_impl 1.0.1", "clap 3.2.23", "ethers-solc", diff --git a/doc/Cargo.toml b/doc/Cargo.toml index ae2ca58ca443..6f98a84054ea 100644 --- a/doc/Cargo.toml +++ b/doc/Cargo.toml @@ -32,3 +32,6 @@ rayon = "1.5.1" itertools = "0.10.3" toml = "0.5" auto_impl = "1" + +[dev-dependencies] +assert_matches = "1.5.0" diff --git a/doc/src/builder.rs b/doc/src/builder.rs index d34ed51f6e2b..812925c8aa0e 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -11,7 +11,7 @@ use std::{ }; use crate::{ - format::DocFormat, + format::AsDoc, output::DocOutput, parser::{ParseItem, ParseSource, Parser}, writer::BufWriter, @@ -144,7 +144,7 @@ impl DocBuilder { // Write doc files for file in files { - let doc_content = file.source.doc()?; + let doc_content = file.source.as_doc()?; fs::create_dir_all( file.target_path.parent().ok_or(eyre::format_err!("empty target path; noop"))?, )?; @@ -246,7 +246,8 @@ impl DocBuilder { .join(format!("contract.{}.md", base_contract.name)), ); return Ok(Some( - DocOutput::Link(&base.doc()?, &path.display().to_string()).doc()?, + DocOutput::Link(&base.as_doc()?, &path.display().to_string()) + .as_doc()?, )) } } diff --git a/doc/src/as_code.rs b/doc/src/format/as_code.rs similarity index 97% rename from doc/src/as_code.rs rename to doc/src/format/as_code.rs index 9206f8c10c90..faada228e5da 100644 --- a/doc/src/as_code.rs +++ b/doc/src/format/as_code.rs @@ -6,11 +6,10 @@ use solang_parser::pt::{ StructDefinition, Type, VariableAttribute, VariableDeclaration, VariableDefinition, }; -/// Display Solidity parse tree unit as code string. -/// [AsCode::as_code] formats the unit into -/// a valid Solidity code block. +/// Display Solidity parse tree item as code string. #[auto_impl::auto_impl(&)] -pub(crate) trait AsCode { +pub trait AsCode { + /// Formats a parse tree item into a valid Solidity code block. fn as_code(&self) -> String; } diff --git a/doc/src/format.rs b/doc/src/format/as_doc.rs similarity index 79% rename from doc/src/format.rs rename to doc/src/format/as_doc.rs index 2429ea63790a..91327bc112bd 100644 --- a/doc/src/format.rs +++ b/doc/src/format/as_doc.rs @@ -1,102 +1,57 @@ use crate::{ helpers::{comments_by_tag, exclude_comment_tags}, - output::DocOutput, parser::{ParseItem, ParseSource}, writer::BufWriter, }; use itertools::Itertools; -use solang_parser::{ - doccomment::DocCommentTag, - pt::{ - Base, EnumDefinition, ErrorDefinition, EventDefinition, FunctionDefinition, - StructDefinition, VariableDefinition, - }, -}; +use solang_parser::{doccomment::DocCommentTag, pt::Base}; -pub(crate) type DocResult = Result; +/// The result of [Asdoc::as_doc] method. +pub type AsDocResult = Result; +/// A trait for formatting a parse unit as documentation. #[auto_impl::auto_impl(&)] -pub(crate) trait DocFormat { - fn doc(&self) -> DocResult; +pub trait AsDoc { + /// Formats a parse tree item into a doc string. + fn as_doc(&self) -> AsDocResult; } -impl DocFormat for String { - fn doc(&self) -> DocResult { +impl AsDoc for String { + fn as_doc(&self) -> AsDocResult { Ok(self.to_owned()) } } -impl DocFormat for DocCommentTag { - fn doc(&self) -> DocResult { +impl AsDoc for DocCommentTag { + fn as_doc(&self) -> AsDocResult { Ok(self.value.to_owned()) } } -impl DocFormat for Vec<&DocCommentTag> { - fn doc(&self) -> DocResult { - Ok(self.iter().map(|c| DocCommentTag::doc(*c)).collect::, _>>()?.join("\n\n")) +impl AsDoc for Vec<&DocCommentTag> { + fn as_doc(&self) -> AsDocResult { + Ok(self + .iter() + .map(|c| DocCommentTag::as_doc(*c)) + .collect::, _>>()? + .join("\n\n")) } } -impl DocFormat for Vec { - fn doc(&self) -> DocResult { - Ok(self.iter().map(DocCommentTag::doc).collect::, _>>()?.join("\n\n")) +impl AsDoc for Vec { + fn as_doc(&self) -> AsDocResult { + Ok(self.iter().map(DocCommentTag::as_doc).collect::, _>>()?.join("\n\n")) } } -// TODO: remove? -impl DocFormat for Base { - fn doc(&self) -> DocResult { +impl AsDoc for Base { + fn as_doc(&self) -> AsDocResult { Ok(self.name.identifiers.iter().map(|ident| ident.name.to_owned()).join(".")) } } -impl DocFormat for Vec { - fn doc(&self) -> DocResult { - Ok(self.iter().map(|base| base.doc()).collect::, _>>()?.join(", ")) - } -} - -// TODO: remove -impl DocFormat for FunctionDefinition { - fn doc(&self) -> DocResult { - let name = self.name.as_ref().map_or(self.ty.to_string(), |n| n.name.to_owned()); - DocOutput::H3(&name).doc() - } -} - -impl DocFormat for EventDefinition { - fn doc(&self) -> DocResult { - DocOutput::H3(&self.name.name).doc() - } -} - -impl DocFormat for ErrorDefinition { - fn doc(&self) -> DocResult { - DocOutput::H3(&self.name.name).doc() - } -} - -impl DocFormat for StructDefinition { - fn doc(&self) -> DocResult { - DocOutput::H3(&self.name.name).doc() - } -} - -impl DocFormat for EnumDefinition { - fn doc(&self) -> DocResult { - DocOutput::H3(&self.name.name).doc() - } -} - -impl DocFormat for VariableDefinition { - fn doc(&self) -> DocResult { - DocOutput::H3(&self.name.name).doc() - } -} - -impl DocFormat for ParseItem { - fn doc(&self) -> DocResult { +impl AsDoc for ParseItem { + fn as_doc(&self) -> AsDocResult { let mut writer = BufWriter::default(); match &self.source { @@ -110,14 +65,14 @@ impl DocFormat for ParseItem { let bases = contract .base .iter() - .map(|base| base.doc()) + .map(|base| base.as_doc()) .collect::, _>>()?; writer.write_bold("Inherits:")?; writer.write_raw(bases.join(", "))?; writer.writeln()?; } - writer.write_raw(self.comments.doc()?)?; + writer.write_raw(self.comments.as_doc()?)?; if let Some(state_vars) = self.variables() { writer.write_subtitle("State Variables")?; @@ -137,7 +92,7 @@ impl DocFormat for ParseItem { // Write function docs writer.write_raw( - exclude_comment_tags(comments, vec!["param", "return"]).doc()?, + exclude_comment_tags(comments, vec!["param", "return"]).as_doc()?, )?; // Write function header @@ -239,7 +194,7 @@ impl DocFormat for ParseItem { // Write function docs writer.write_raw( - exclude_comment_tags(&self.comments, vec!["param", "return"]).doc()?, + exclude_comment_tags(&self.comments, vec!["param", "return"]).as_doc()?, )?; // Write function header diff --git a/doc/src/format/mod.rs b/doc/src/format/mod.rs new file mode 100644 index 000000000000..8df00401aa8f --- /dev/null +++ b/doc/src/format/mod.rs @@ -0,0 +1,7 @@ +//! The module for formatting various parse tree items + +mod as_code; +mod as_doc; + +pub use as_code::AsCode; +pub use as_doc::{AsDoc, AsDocResult}; diff --git a/doc/src/lib.rs b/doc/src/lib.rs index a9ecf06a2a87..ebb12502a0c2 100644 --- a/doc/src/lib.rs +++ b/doc/src/lib.rs @@ -9,7 +9,6 @@ //! //! See [DocBuilder] -mod as_code; mod builder; mod format; mod helpers; @@ -17,5 +16,11 @@ mod output; mod parser; mod writer; +/// The documentation builder. pub use builder::DocBuilder; + +/// Solidity parser and related output items. pub use parser::{error, ParseItem, ParseSource, Parser}; + +/// Traits for formatting items into doc output/ +pub use format::{AsCode, AsDoc, AsDocResult}; diff --git a/doc/src/output.rs b/doc/src/output.rs index df08fd7d8069..d521ff445aaa 100644 --- a/doc/src/output.rs +++ b/doc/src/output.rs @@ -1,4 +1,4 @@ -use crate::format::{DocFormat, DocResult}; +use crate::format::{AsDoc, AsDocResult}; /// TODO: rename pub(crate) enum DocOutput<'a> { @@ -10,8 +10,8 @@ pub(crate) enum DocOutput<'a> { CodeBlock(&'a str, &'a str), } -impl<'a> DocFormat for DocOutput<'a> { - fn doc(&self) -> DocResult { +impl<'a> AsDoc for DocOutput<'a> { + fn as_doc(&self) -> AsDocResult { let doc = match self { Self::H1(val) => format!("# {val}"), Self::H2(val) => format!("## {val}"), @@ -26,6 +26,6 @@ impl<'a> DocFormat for DocOutput<'a> { impl<'a> std::fmt::Display for DocOutput<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("{}", self.doc()?)) + f.write_fmt(format_args!("{}", self.as_doc()?)) } } diff --git a/doc/src/parser/item.rs b/doc/src/parser/item.rs index bde9b0464c8d..89b1261d2530 100644 --- a/doc/src/parser/item.rs +++ b/doc/src/parser/item.rs @@ -39,12 +39,16 @@ macro_rules! filter_children_fn { } impl ParseItem { - filter_children_fn!(pub fn variables(&self, Variable) -> VariableDefinition); - filter_children_fn!(pub fn functions(&self, Function) -> FunctionDefinition); - filter_children_fn!(pub fn events(&self, Event) -> EventDefinition); - filter_children_fn!(pub fn errors(&self, Error) -> ErrorDefinition); - filter_children_fn!(pub fn structs(&self, Struct) -> StructDefinition); - filter_children_fn!(pub fn enums(&self, Enum) -> EnumDefinition); + /// Create new instance of [ParseItem]. + pub fn new(source: ParseSource) -> Self { + Self { source, comments: Default::default(), children: Default::default() } + } + + /// Set comments on the [ParseItem]. + pub fn with_comments(mut self, comments: Vec) -> Self { + self.comments = comments; + self + } /// Format the item's filename. pub fn filename(&self) -> String { @@ -60,6 +64,13 @@ impl ParseItem { let ident = self.source.ident(); format!("{prefix}.{ident}.md") } + + filter_children_fn!(pub fn variables(&self, Variable) -> VariableDefinition); + filter_children_fn!(pub fn functions(&self, Function) -> FunctionDefinition); + filter_children_fn!(pub fn events(&self, Event) -> EventDefinition); + filter_children_fn!(pub fn errors(&self, Error) -> ErrorDefinition); + filter_children_fn!(pub fn structs(&self, Struct) -> StructDefinition); + filter_children_fn!(pub fn enums(&self, Enum) -> EnumDefinition); } /// A wrapper type around pt token. diff --git a/doc/src/parser/mod.rs b/doc/src/parser/mod.rs index 918eb47f725c..dbe203cfdf2b 100644 --- a/doc/src/parser/mod.rs +++ b/doc/src/parser/mod.rs @@ -61,7 +61,8 @@ impl Parser { Ok(parent) } - /// Adds a child element to the parent parsed item. + /// Adds a child element to the parent item if it exists. + /// Otherwise the element will be added to a top-level items collection. /// Moves the doc comment pointer to the end location of the child element. fn add_element_to_parent(&mut self, source: ParseSource, loc: Loc) { let child = ParseItem { source, comments: self.parse_docs(loc.start()), children: vec![] }; @@ -93,12 +94,15 @@ impl Visitor for Parser { for source in source_unit.0.iter_mut() { match source { SourceUnitPart::ContractDefinition(def) => { - let contract = ParseItem { - source: ParseSource::Contract(def.clone()), - comments: self.parse_docs(def.loc.start()), - children: vec![], - }; + // Create new contract parse item. + let source = ParseSource::Contract(def.clone()); + let comments = self.parse_docs(def.loc.start()); + let contract = ParseItem::new(source).with_comments(comments); + + // Move the doc pointer to the contract location start. self.start_at = def.loc.start(); + + // Parse child elements with current contract as parent let contract = self.with_parent(contract, |doc| { def.parts .iter_mut() @@ -107,7 +111,10 @@ impl Visitor for Parser { Ok(()) })?; + // Move the doc pointer to the contract location end. self.start_at = def.loc.end(); + + // Add contract to the parsed items. self.items.push(contract); } SourceUnitPart::FunctionDefinition(func) => self.visit_function(func)?, @@ -115,6 +122,7 @@ impl Visitor for Parser { SourceUnitPart::ErrorDefinition(error) => self.visit_error(error)?, SourceUnitPart::StructDefinition(structure) => self.visit_struct(structure)?, SourceUnitPart::EnumDefinition(enumerable) => self.visit_enum(enumerable)?, + SourceUnitPart::VariableDefinition(var) => self.visit_var_definition(var)?, _ => {} }; } @@ -153,22 +161,108 @@ impl Visitor for Parser { } } -// #[cfg(test)] -// mod tests { -// use super::*; -// use std::{fs, path::PathBuf}; - -// // TODO: write tests w/ sol source code - -// #[test] -// fn parse_docs() { -// let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) -// .join("testdata") -// .join("oz-governance") -// .join("Governor.sol"); -// let target = fs::read_to_string(path).unwrap(); -// let (mut source_pt, source_comments) = solang_parser::parse(&target, 1).unwrap(); -// let mut d = DocParser::new(source_comments); -// assert!(source_pt.visit(&mut d).is_ok()); -// } -// } +#[cfg(test)] +mod tests { + use super::*; + use assert_matches::assert_matches; + use solang_parser::parse; + + #[inline] + fn parse_source(src: &str) -> Vec { + let (mut source, comments) = parse(src, 0).expect("failed to parse source"); + let mut doc = Parser::new(comments); + source.visit(&mut doc).expect("failed to visit source"); + doc.items() + } + + macro_rules! test_single_unit { + ($test:ident, $src:expr, $variant:ident $identity:expr) => { + #[test] + fn $test() { + let items = parse_source($src); + assert_eq!(items.len(), 1); + let item = items.first().unwrap(); + assert!(item.comments.is_empty()); + assert!(item.children.is_empty()); + assert_eq!(item.source.ident(), $identity); + assert_matches!(item.source, ParseSource::$variant(_)); + } + }; + } + + #[test] + fn empty_source() { + assert_eq!(parse_source(""), vec![]); + } + + test_single_unit!(single_function, "function someFn() { }", Function "someFn"); + test_single_unit!(single_variable, "uint256 constant VALUE = 0;", Variable "VALUE"); + test_single_unit!(single_event, "event SomeEvent();", Event "SomeEvent"); + test_single_unit!(single_error, "error SomeError();", Error "SomeError"); + test_single_unit!(single_struct, "struct SomeStruct { }", Struct "SomeStruct"); + test_single_unit!(single_enum, "enum SomeEnum { SOME, OTHER }", Enum "SomeEnum"); + test_single_unit!(single_contract, "contract Contract { }", Contract "Contract"); + + #[test] + fn multiple_shallow_contracts() { + let items = parse_source( + r#" + contract A { } + contract B { } + contract C { } + "#, + ); + assert_eq!(items.len(), 3); + + let first_item = items.get(0).unwrap(); + assert_matches!(first_item.source, ParseSource::Contract(_)); + assert_eq!(first_item.source.ident(), "A"); + + let first_item = items.get(1).unwrap(); + assert_matches!(first_item.source, ParseSource::Contract(_)); + assert_eq!(first_item.source.ident(), "B"); + + let first_item = items.get(2).unwrap(); + assert_matches!(first_item.source, ParseSource::Contract(_)); + assert_eq!(first_item.source.ident(), "C"); + } + + #[test] + fn contract_with_children_items() { + let items = parse_source( + r#" + event TopLevelEvent(); + + contract Contract { + event ContractEvent(); + error ContractError(); + struct ContractStruct { } + enum ContractEnum { } + + uint256 constant CONTRACT_CONSTANT; + bool contractVar; + + function contractFunction(uint256) external returns (uint256) { + bool localVar; // must be ignored + } + } + "#, + ); + + assert_eq!(items.len(), 2); + + let event = items.get(0).unwrap(); + assert!(event.comments.is_empty()); + assert!(event.children.is_empty()); + assert_eq!(event.source.ident(), "TopLevelEvent"); + assert_matches!(event.source, ParseSource::Event(_)); + + let contract = items.get(1).unwrap(); + assert!(contract.comments.is_empty()); + assert_eq!(contract.children.len(), 7); + assert_eq!(contract.source.ident(), "Contract"); + assert_matches!(contract.source, ParseSource::Contract(_)); + assert!(contract.children.iter().all(|ch| ch.children.is_empty())); + assert!(contract.children.iter().all(|ch| ch.comments.is_empty())); + } +} diff --git a/doc/src/writer.rs b/doc/src/writer.rs index 67e797908b32..0767e193c78d 100644 --- a/doc/src/writer.rs +++ b/doc/src/writer.rs @@ -2,7 +2,10 @@ use itertools::Itertools; use solang_parser::{doccomment::DocCommentTag, pt::Parameter}; use std::fmt::{self, Display, Write}; -use crate::{as_code::AsCode, format::DocFormat, output::DocOutput}; +use crate::{ + format::{AsCode, AsDoc}, + output::DocOutput, +}; /// TODO: comments #[derive(Default)] @@ -51,7 +54,7 @@ impl BufWriter { depth: usize, ) -> fmt::Result { let link = DocOutput::Link(name, path); - self.write_list_item(&link.doc()?, depth) + self.write_list_item(&link.as_doc()?, depth) } pub(crate) fn write_code(&mut self, item: T) -> fmt::Result { @@ -61,12 +64,12 @@ impl BufWriter { } // TODO: revise - pub(crate) fn write_section( + pub(crate) fn write_section( &mut self, item: T, comments: &Vec, ) -> fmt::Result { - self.write_raw(&comments.doc()?)?; + self.write_raw(&comments.as_doc()?)?; self.writeln()?; self.write_code(item)?; self.writeln()?; diff --git a/doc/testdata/oz-governance/Governor.sol b/doc/testdata/oz-governance/Governor.sol deleted file mode 100644 index ae34135911c0..000000000000 --- a/doc/testdata/oz-governance/Governor.sol +++ /dev/null @@ -1,596 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (governance/Governor.sol) - -pragma solidity ^0.8.0; - -import "../token/ERC721/IERC721Receiver.sol"; -import "../token/ERC1155/IERC1155Receiver.sol"; -import "../utils/cryptography/ECDSA.sol"; -import "../utils/cryptography/draft-EIP712.sol"; -import "../utils/introspection/ERC165.sol"; -import "../utils/math/SafeCast.sol"; -import "../utils/structs/DoubleEndedQueue.sol"; -import "../utils/Address.sol"; -import "../utils/Context.sol"; -import "../utils/Timers.sol"; -import "./IGovernor.sol"; - -/** - * @dev Core of the governance system, designed to be extended though various modules. - * - * This contract is abstract and requires several function to be implemented in various modules: - * - * - A counting module must implement {quorum}, {_quorumReached}, {_voteSucceeded} and {_countVote} - * - A voting module must implement {_getVotes} - * - Additionanly, the {votingPeriod} must also be implemented - * - * _Available since v4.3._ - */ -abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receiver, IERC1155Receiver { - using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque; - using SafeCast for uint256; - using Timers for Timers.BlockNumber; - - bytes32 public constant BALLOT_TYPEHASH = keccak256("Ballot(uint256 proposalId,uint8 support)"); - bytes32 public constant EXTENDED_BALLOT_TYPEHASH = - keccak256("ExtendedBallot(uint256 proposalId,uint8 support,string reason,bytes params)"); - - struct ProposalCore { - Timers.BlockNumber voteStart; - Timers.BlockNumber voteEnd; - bool executed; - bool canceled; - } - - string private _name; - - mapping(uint256 => ProposalCore) private _proposals; - - // This queue keeps track of the governor operating on itself. Calls to functions protected by the - // {onlyGovernance} modifier needs to be whitelisted in this queue. Whitelisting is set in {_beforeExecute}, - // consumed by the {onlyGovernance} modifier and eventually reset in {_afterExecute}. This ensures that the - // execution of {onlyGovernance} protected calls can only be achieved through successful proposals. - DoubleEndedQueue.Bytes32Deque private _governanceCall; - - /** - * @dev Restricts a function so it can only be executed through governance proposals. For example, governance - * parameter setters in {GovernorSettings} are protected using this modifier. - * - * The governance executing address may be different from the Governor's own address, for example it could be a - * timelock. This can be customized by modules by overriding {_executor}. The executor is only able to invoke these - * functions during the execution of the governor's {execute} function, and not under any other circumstances. Thus, - * for example, additional timelock proposers are not able to change governance parameters without going through the - * governance protocol (since v4.6). - */ - modifier onlyGovernance() { - require(_msgSender() == _executor(), "Governor: onlyGovernance"); - if (_executor() != address(this)) { - bytes32 msgDataHash = keccak256(_msgData()); - // loop until popping the expected operation - throw if deque is empty (operation not authorized) - while (_governanceCall.popFront() != msgDataHash) {} - } - _; - } - - /** - * @dev Sets the value for {name} and {version} - */ - constructor(string memory name_) EIP712(name_, version()) { - _name = name_; - } - - /** - * @dev Function to receive ETH that will be handled by the governor (disabled if executor is a third party contract) - */ - receive() external payable virtual { - require(_executor() == address(this)); - } - - /** - * @dev See {IERC165-supportsInterface}. - */ - function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) { - // In addition to the current interfaceId, also support previous version of the interfaceId that did not - // include the castVoteWithReasonAndParams() function as standard - return - interfaceId == - (type(IGovernor).interfaceId ^ - this.castVoteWithReasonAndParams.selector ^ - this.castVoteWithReasonAndParamsBySig.selector ^ - this.getVotesWithParams.selector) || - interfaceId == type(IGovernor).interfaceId || - interfaceId == type(IERC1155Receiver).interfaceId || - super.supportsInterface(interfaceId); - } - - /** - * @dev See {IGovernor-name}. - */ - function name() public view virtual override returns (string memory) { - return _name; - } - - /** - * @dev See {IGovernor-version}. - */ - function version() public view virtual override returns (string memory) { - return "1"; - } - - /** - * @dev See {IGovernor-hashProposal}. - * - * The proposal id is produced by hashing the ABI encoded `targets` array, the `values` array, the `calldatas` array - * and the descriptionHash (bytes32 which itself is the keccak256 hash of the description string). This proposal id - * can be produced from the proposal data which is part of the {ProposalCreated} event. It can even be computed in - * advance, before the proposal is submitted. - * - * Note that the chainId and the governor address are not part of the proposal id computation. Consequently, the - * same proposal (with same operation and same description) will have the same id if submitted on multiple governors - * across multiple networks. This also means that in order to execute the same operation twice (on the same - * governor) the proposer will have to change the description in order to avoid proposal id conflicts. - */ - function hashProposal( - address[] memory targets, - uint256[] memory values, - bytes[] memory calldatas, - bytes32 descriptionHash - ) public pure virtual override returns (uint256) { - return uint256(keccak256(abi.encode(targets, values, calldatas, descriptionHash))); - } - - /** - * @dev See {IGovernor-state}. - */ - function state(uint256 proposalId) public view virtual override returns (ProposalState) { - ProposalCore storage proposal = _proposals[proposalId]; - - if (proposal.executed) { - return ProposalState.Executed; - } - - if (proposal.canceled) { - return ProposalState.Canceled; - } - - uint256 snapshot = proposalSnapshot(proposalId); - - if (snapshot == 0) { - revert("Governor: unknown proposal id"); - } - - if (snapshot >= block.number) { - return ProposalState.Pending; - } - - uint256 deadline = proposalDeadline(proposalId); - - if (deadline >= block.number) { - return ProposalState.Active; - } - - if (_quorumReached(proposalId) && _voteSucceeded(proposalId)) { - return ProposalState.Succeeded; - } else { - return ProposalState.Defeated; - } - } - - /** - * @dev See {IGovernor-proposalSnapshot}. - */ - function proposalSnapshot(uint256 proposalId) public view virtual override returns (uint256) { - return _proposals[proposalId].voteStart.getDeadline(); - } - - /** - * @dev See {IGovernor-proposalDeadline}. - */ - function proposalDeadline(uint256 proposalId) public view virtual override returns (uint256) { - return _proposals[proposalId].voteEnd.getDeadline(); - } - - /** - * @dev Part of the Governor Bravo's interface: _"The number of votes required in order for a voter to become a proposer"_. - */ - function proposalThreshold() public view virtual returns (uint256) { - return 0; - } - - /** - * @dev Amount of votes already cast passes the threshold limit. - */ - function _quorumReached(uint256 proposalId) internal view virtual returns (bool); - - /** - * @dev Is the proposal successful or not. - */ - function _voteSucceeded(uint256 proposalId) internal view virtual returns (bool); - - /** - * @dev Get the voting weight of `account` at a specific `blockNumber`, for a vote as described by `params`. - */ - function _getVotes( - address account, - uint256 blockNumber, - bytes memory params - ) internal view virtual returns (uint256); - - /** - * @dev Register a vote for `proposalId` by `account` with a given `support`, voting `weight` and voting `params`. - * - * Note: Support is generic and can represent various things depending on the voting system used. - */ - function _countVote( - uint256 proposalId, - address account, - uint8 support, - uint256 weight, - bytes memory params - ) internal virtual; - - /** - * @dev Default additional encoded parameters used by castVote methods that don't include them - * - * Note: Should be overridden by specific implementations to use an appropriate value, the - * meaning of the additional params, in the context of that implementation - */ - function _defaultParams() internal view virtual returns (bytes memory) { - return ""; - } - - /** - * @dev See {IGovernor-propose}. - */ - function propose( - address[] memory targets, - uint256[] memory values, - bytes[] memory calldatas, - string memory description - ) public virtual override returns (uint256) { - require( - getVotes(_msgSender(), block.number - 1) >= proposalThreshold(), - "Governor: proposer votes below proposal threshold" - ); - - uint256 proposalId = hashProposal(targets, values, calldatas, keccak256(bytes(description))); - - require(targets.length == values.length, "Governor: invalid proposal length"); - require(targets.length == calldatas.length, "Governor: invalid proposal length"); - require(targets.length > 0, "Governor: empty proposal"); - - ProposalCore storage proposal = _proposals[proposalId]; - require(proposal.voteStart.isUnset(), "Governor: proposal already exists"); - - uint64 snapshot = block.number.toUint64() + votingDelay().toUint64(); - uint64 deadline = snapshot + votingPeriod().toUint64(); - - proposal.voteStart.setDeadline(snapshot); - proposal.voteEnd.setDeadline(deadline); - - emit ProposalCreated( - proposalId, - _msgSender(), - targets, - values, - new string[](targets.length), - calldatas, - snapshot, - deadline, - description - ); - - return proposalId; - } - - /** - * @dev See {IGovernor-execute}. - */ - function execute( - address[] memory targets, - uint256[] memory values, - bytes[] memory calldatas, - bytes32 descriptionHash - ) public payable virtual override returns (uint256) { - uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash); - - ProposalState status = state(proposalId); - require( - status == ProposalState.Succeeded || status == ProposalState.Queued, - "Governor: proposal not successful" - ); - _proposals[proposalId].executed = true; - - emit ProposalExecuted(proposalId); - - _beforeExecute(proposalId, targets, values, calldatas, descriptionHash); - _execute(proposalId, targets, values, calldatas, descriptionHash); - _afterExecute(proposalId, targets, values, calldatas, descriptionHash); - - return proposalId; - } - - /** - * @dev Internal execution mechanism. Can be overridden to implement different execution mechanism - */ - function _execute( - uint256, /* proposalId */ - address[] memory targets, - uint256[] memory values, - bytes[] memory calldatas, - bytes32 /*descriptionHash*/ - ) internal virtual { - string memory errorMessage = "Governor: call reverted without message"; - for (uint256 i = 0; i < targets.length; ++i) { - (bool success, bytes memory returndata) = targets[i].call{value: values[i]}(calldatas[i]); - Address.verifyCallResult(success, returndata, errorMessage); - } - } - - /** - * @dev Hook before execution is triggered. - */ - function _beforeExecute( - uint256, /* proposalId */ - address[] memory targets, - uint256[] memory, /* values */ - bytes[] memory calldatas, - bytes32 /*descriptionHash*/ - ) internal virtual { - if (_executor() != address(this)) { - for (uint256 i = 0; i < targets.length; ++i) { - if (targets[i] == address(this)) { - _governanceCall.pushBack(keccak256(calldatas[i])); - } - } - } - } - - /** - * @dev Hook after execution is triggered. - */ - function _afterExecute( - uint256, /* proposalId */ - address[] memory, /* targets */ - uint256[] memory, /* values */ - bytes[] memory, /* calldatas */ - bytes32 /*descriptionHash*/ - ) internal virtual { - if (_executor() != address(this)) { - if (!_governanceCall.empty()) { - _governanceCall.clear(); - } - } - } - - /** - * @dev Internal cancel mechanism: locks up the proposal timer, preventing it from being re-submitted. Marks it as - * canceled to allow distinguishing it from executed proposals. - * - * Emits a {IGovernor-ProposalCanceled} event. - */ - function _cancel( - address[] memory targets, - uint256[] memory values, - bytes[] memory calldatas, - bytes32 descriptionHash - ) internal virtual returns (uint256) { - uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash); - ProposalState status = state(proposalId); - - require( - status != ProposalState.Canceled && status != ProposalState.Expired && status != ProposalState.Executed, - "Governor: proposal not active" - ); - _proposals[proposalId].canceled = true; - - emit ProposalCanceled(proposalId); - - return proposalId; - } - - /** - * @dev See {IGovernor-getVotes}. - */ - function getVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) { - return _getVotes(account, blockNumber, _defaultParams()); - } - - /** - * @dev See {IGovernor-getVotesWithParams}. - */ - function getVotesWithParams( - address account, - uint256 blockNumber, - bytes memory params - ) public view virtual override returns (uint256) { - return _getVotes(account, blockNumber, params); - } - - /** - * @dev See {IGovernor-castVote}. - */ - function castVote(uint256 proposalId, uint8 support) public virtual override returns (uint256) { - address voter = _msgSender(); - return _castVote(proposalId, voter, support, ""); - } - - /** - * @dev See {IGovernor-castVoteWithReason}. - */ - function castVoteWithReason( - uint256 proposalId, - uint8 support, - string calldata reason - ) public virtual override returns (uint256) { - address voter = _msgSender(); - return _castVote(proposalId, voter, support, reason); - } - - /** - * @dev See {IGovernor-castVoteWithReasonAndParams}. - */ - function castVoteWithReasonAndParams( - uint256 proposalId, - uint8 support, - string calldata reason, - bytes memory params - ) public virtual override returns (uint256) { - address voter = _msgSender(); - return _castVote(proposalId, voter, support, reason, params); - } - - /** - * @dev See {IGovernor-castVoteBySig}. - */ - function castVoteBySig( - uint256 proposalId, - uint8 support, - uint8 v, - bytes32 r, - bytes32 s - ) public virtual override returns (uint256) { - address voter = ECDSA.recover( - _hashTypedDataV4(keccak256(abi.encode(BALLOT_TYPEHASH, proposalId, support))), - v, - r, - s - ); - return _castVote(proposalId, voter, support, ""); - } - - /** - * @dev See {IGovernor-castVoteWithReasonAndParamsBySig}. - */ - function castVoteWithReasonAndParamsBySig( - uint256 proposalId, - uint8 support, - string calldata reason, - bytes memory params, - uint8 v, - bytes32 r, - bytes32 s - ) public virtual override returns (uint256) { - address voter = ECDSA.recover( - _hashTypedDataV4( - keccak256( - abi.encode( - EXTENDED_BALLOT_TYPEHASH, - proposalId, - support, - keccak256(bytes(reason)), - keccak256(params) - ) - ) - ), - v, - r, - s - ); - - return _castVote(proposalId, voter, support, reason, params); - } - - /** - * @dev Internal vote casting mechanism: Check that the vote is pending, that it has not been cast yet, retrieve - * voting weight using {IGovernor-getVotes} and call the {_countVote} internal function. Uses the _defaultParams(). - * - * Emits a {IGovernor-VoteCast} event. - */ - function _castVote( - uint256 proposalId, - address account, - uint8 support, - string memory reason - ) internal virtual returns (uint256) { - return _castVote(proposalId, account, support, reason, _defaultParams()); - } - - /** - * @dev Internal vote casting mechanism: Check that the vote is pending, that it has not been cast yet, retrieve - * voting weight using {IGovernor-getVotes} and call the {_countVote} internal function. - * - * Emits a {IGovernor-VoteCast} event. - */ - function _castVote( - uint256 proposalId, - address account, - uint8 support, - string memory reason, - bytes memory params - ) internal virtual returns (uint256) { - ProposalCore storage proposal = _proposals[proposalId]; - require(state(proposalId) == ProposalState.Active, "Governor: vote not currently active"); - - uint256 weight = _getVotes(account, proposal.voteStart.getDeadline(), params); - _countVote(proposalId, account, support, weight, params); - - if (params.length == 0) { - emit VoteCast(account, proposalId, support, weight, reason); - } else { - emit VoteCastWithParams(account, proposalId, support, weight, reason, params); - } - - return weight; - } - - /** - * @dev Relays a transaction or function call to an arbitrary target. In cases where the governance executor - * is some contract other than the governor itself, like when using a timelock, this function can be invoked - * in a governance proposal to recover tokens or Ether that was sent to the governor contract by mistake. - * Note that if the executor is simply the governor itself, use of `relay` is redundant. - */ - function relay( - address target, - uint256 value, - bytes calldata data - ) external virtual onlyGovernance { - Address.functionCallWithValue(target, data, value); - } - - /** - * @dev Address through which the governor executes action. Will be overloaded by module that execute actions - * through another contract such as a timelock. - */ - function _executor() internal view virtual returns (address) { - return address(this); - } - - /** - * @dev See {IERC721Receiver-onERC721Received}. - */ - function onERC721Received( - address, - address, - uint256, - bytes memory - ) public virtual override returns (bytes4) { - return this.onERC721Received.selector; - } - - /** - * @dev See {IERC1155Receiver-onERC1155Received}. - */ - function onERC1155Received( - address, - address, - uint256, - uint256, - bytes memory - ) public virtual override returns (bytes4) { - return this.onERC1155Received.selector; - } - - /** - * @dev See {IERC1155Receiver-onERC1155BatchReceived}. - */ - function onERC1155BatchReceived( - address, - address, - uint256[] memory, - uint256[] memory, - bytes memory - ) public virtual override returns (bytes4) { - return this.onERC1155BatchReceived.selector; - } -} From db883446ca5a90ece7c6ed862e4c1490181b8d32 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Fri, 6 Jan 2023 11:50:00 +0200 Subject: [PATCH 25/67] misc --- doc/src/builder.rs | 20 +++++++++++--------- doc/src/parser/mod.rs | 21 ++++++++++++++------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/doc/src/builder.rs b/doc/src/builder.rs index 812925c8aa0e..3a310d00a0df 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -19,14 +19,14 @@ use crate::{ #[derive(Debug)] struct DocFile { - source: ParseItem, - source_path: PathBuf, + item: ParseItem, + item_path: PathBuf, target_path: PathBuf, } impl DocFile { - fn new(source: ParseItem, source_path: PathBuf, target_path: PathBuf) -> Self { - Self { source_path, source, target_path } + fn new(item: ParseItem, item_path: PathBuf, target_path: PathBuf) -> Self { + Self { item_path, item, target_path } } } @@ -88,9 +88,12 @@ impl DocBuilder { // Flatten and sort the results let files = files.into_iter().flatten().sorted_by(|file1, file2| { - file1.source_path.display().to_string().cmp(&file2.source_path.display().to_string()) + file1.item_path.display().to_string().cmp(&file2.item_path.display().to_string()) }); + // Apply preprocessors to files + // TODO: + // Write mdbook related files self.write_mdbook(files.collect::>())?; @@ -118,7 +121,6 @@ impl DocBuilder { let mut summary = BufWriter::default(); summary.write_title("Summary")?; summary.write_link_list_item("README", Self::README, 0)?; - // TODO: self.write_summary_section(&mut summary, &files.iter().collect::>(), None, 0)?; fs::write(out_dir_src.join(Self::SUMMARY), summary.finish())?; @@ -144,7 +146,7 @@ impl DocBuilder { // Write doc files for file in files { - let doc_content = file.source.as_doc()?; + let doc_content = file.item.as_doc()?; fs::create_dir_all( file.target_path.parent().ok_or(eyre::format_err!("empty target path; noop"))?, )?; @@ -182,7 +184,7 @@ impl DocBuilder { // Group entries by path depth let mut grouped = HashMap::new(); for file in files { - let path = file.source_path.strip_prefix(&self.root)?; + let path = file.item_path.strip_prefix(&self.root)?; let key = path.iter().take(depth + 1).collect::(); grouped.entry(key).or_insert_with(Vec::new).push(*file); } @@ -204,7 +206,7 @@ impl DocBuilder { for (path, files) in grouped { if path.extension().map(|ext| ext.eq(Self::SOL_EXT)).unwrap_or_default() { for file in files { - let ident = file.source.source.ident(); + let ident = file.item.source.ident(); let readme_path = Path::new("/").join(&path).display().to_string(); readme.write_link_list_item(&ident, &readme_path, 0)?; diff --git a/doc/src/parser/mod.rs b/doc/src/parser/mod.rs index dbe203cfdf2b..65a372409f02 100644 --- a/doc/src/parser/mod.rs +++ b/doc/src/parser/mod.rs @@ -21,16 +21,21 @@ pub use item::{ParseItem, ParseSource}; /// [ParseItem]s can be accessed by calling [Parser::items]. #[derive(Debug, Default)] pub struct Parser { - items: Vec, + /// Initial comments from solang parser. comments: Vec, - start_at: usize, + /// Parser context. context: ParserContext, + /// Parsed results. + items: Vec, } -/// [Parser]'s context. Holds information about current parent that's being visited. +/// [Parser] context. #[derive(Debug, Default)] struct ParserContext { + /// Current visited parent. parent: Option, + /// Current start pointer for parsing doc comments. + doc_start_loc: usize, } impl Parser { @@ -71,13 +76,13 @@ impl Parser { } else { self.items.push(child); } - self.start_at = loc.end(); + self.context.doc_start_loc = loc.end(); } /// Parse the doc comments from the current start location. fn parse_docs(&mut self, end: usize) -> Vec { let mut res = vec![]; - for comment in parse_doccomments(&self.comments, self.start_at, end) { + for comment in parse_doccomments(&self.comments, self.context.doc_start_loc, end) { match comment { DocComment::Line { comment } => res.push(comment), DocComment::Block { comments } => res.extend(comments.into_iter()), @@ -100,7 +105,7 @@ impl Visitor for Parser { let contract = ParseItem::new(source).with_comments(comments); // Move the doc pointer to the contract location start. - self.start_at = def.loc.start(); + self.context.doc_start_loc = def.loc.start(); // Parse child elements with current contract as parent let contract = self.with_parent(contract, |doc| { @@ -112,7 +117,7 @@ impl Visitor for Parser { })?; // Move the doc pointer to the contract location end. - self.start_at = def.loc.end(); + self.context.doc_start_loc = def.loc.end(); // Add contract to the parsed items. self.items.push(contract); @@ -265,4 +270,6 @@ mod tests { assert!(contract.children.iter().all(|ch| ch.children.is_empty())); assert!(contract.children.iter().all(|ch| ch.comments.is_empty())); } + + // TODO: test regular doc comments & natspec } From 0f3dc3611f2d84d59523fdcddf3f7bb9e5848f4f Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Sat, 7 Jan 2023 11:29:33 +0200 Subject: [PATCH 26/67] writer & preprocessor abstractions, fix inheritance linking (aka another rewrite) --- Cargo.lock | 11 +- cli/src/cmd/forge/doc.rs | 13 +- cli/tests/it/config.rs | 1 + doc/Cargo.toml | 1 + doc/src/builder.rs | 156 ++++++++++--------- doc/src/document.rs | 50 ++++++ doc/src/format/mod.rs | 7 - doc/src/lib.rs | 15 +- doc/src/parser/item.rs | 26 +++- doc/src/preprocessor/contract_inheritance.rs | 55 +++++++ doc/src/preprocessor/mod.rs | 38 +++++ doc/src/{format => writer}/as_code.rs | 0 doc/src/{format => writer}/as_doc.rs | 59 ++++--- doc/src/{ => writer}/helpers.rs | 0 doc/src/{output.rs => writer/markdown.rs} | 17 +- doc/src/writer/mod.rs | 12 ++ doc/src/{ => writer}/writer.rs | 33 ++-- 17 files changed, 358 insertions(+), 136 deletions(-) create mode 100644 doc/src/document.rs delete mode 100644 doc/src/format/mod.rs create mode 100644 doc/src/preprocessor/contract_inheritance.rs create mode 100644 doc/src/preprocessor/mod.rs rename doc/src/{format => writer}/as_code.rs (100%) rename doc/src/{format => writer}/as_doc.rs (80%) rename doc/src/{ => writer}/helpers.rs (100%) rename doc/src/{output.rs => writer/markdown.rs} (69%) create mode 100644 doc/src/writer/mod.rs rename doc/src/{ => writer}/writer.rs (83%) diff --git a/Cargo.lock b/Cargo.lock index cd35ef1022f4..ce76794328e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1154,6 +1154,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + [[package]] name = "convert_case" version = "0.6.0" @@ -1534,8 +1540,10 @@ version = "0.99.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ + "convert_case 0.4.0", "proc-macro2", "quote", + "rustc_version", "syn", ] @@ -1970,7 +1978,7 @@ dependencies = [ "bytes", "cargo_metadata", "chrono", - "convert_case", + "convert_case 0.6.0", "elliptic-curve", "ethabi", "generic-array 0.14.6", @@ -2334,6 +2342,7 @@ dependencies = [ "assert_matches", "auto_impl 1.0.1", "clap 3.2.23", + "derive_more", "ethers-solc", "eyre", "forge-fmt", diff --git a/cli/src/cmd/forge/doc.rs b/cli/src/cmd/forge/doc.rs index a04f6f201b42..6aa4c2f9486f 100644 --- a/cli/src/cmd/forge/doc.rs +++ b/cli/src/cmd/forge/doc.rs @@ -33,13 +33,10 @@ impl Cmd for DocArgs { let root = self.root.clone().unwrap_or(find_project_root_path()?); let config = load_config_with_root(self.root.clone()); - let builder = DocBuilder { - root, - sources: config.project_paths().sources, - out: self.out.clone().unwrap_or(config.doc.out.clone()), - title: config.doc.title.clone(), - }; - - builder.build() + DocBuilder::new(root, config.project_paths().sources) + .with_out(self.out.clone().unwrap_or(config.doc.out.clone())) + .with_title(config.doc.title.clone()) + // TODO: .with_preprocessors() + .build() } } diff --git a/cli/tests/it/config.rs b/cli/tests/it/config.rs index 3595291c23cc..e9370e6c0459 100644 --- a/cli/tests/it/config.rs +++ b/cli/tests/it/config.rs @@ -108,6 +108,7 @@ forgetest!(can_extract_config_values, |prj: TestProject, mut cmd: TestCommand| { build_info: false, build_info_path: None, fmt: Default::default(), + doc: Default::default(), fs_permissions: Default::default(), __non_exhaustive: (), __warnings: vec![], diff --git a/doc/Cargo.toml b/doc/Cargo.toml index 6f98a84054ea..f6ed6db2cd21 100644 --- a/doc/Cargo.toml +++ b/doc/Cargo.toml @@ -32,6 +32,7 @@ rayon = "1.5.1" itertools = "0.10.3" toml = "0.5" auto_impl = "1" +derive_more = "0.99" [dev-dependencies] assert_matches = "1.5.0" diff --git a/doc/src/builder.rs b/doc/src/builder.rs index 3a310d00a0df..4f3bdf4d1074 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -2,7 +2,6 @@ use ethers_solc::utils::source_files_iter; use forge_fmt::Visitable; use itertools::Itertools; use rayon::prelude::*; -use solang_parser::pt::Base; use std::{ cmp::Ordering, collections::HashMap, @@ -10,25 +9,7 @@ use std::{ path::{Path, PathBuf}, }; -use crate::{ - format::AsDoc, - output::DocOutput, - parser::{ParseItem, ParseSource, Parser}, - writer::BufWriter, -}; - -#[derive(Debug)] -struct DocFile { - item: ParseItem, - item_path: PathBuf, - target_path: PathBuf, -} - -impl DocFile { - fn new(item: ParseItem, item_path: PathBuf, target_path: PathBuf) -> Self { - Self { item_path, item, target_path } - } -} +use crate::{AsDoc, BufWriter, Document, Parser, Preprocessor}; /// Build Solidity documentation for a project from natspec comments. /// The builder parses the source files using [Parser], @@ -43,13 +24,45 @@ pub struct DocBuilder { pub out: PathBuf, /// The documentation title. pub title: String, + /// THe + pub preprocessors: Vec>, } // TODO: consider using `tfio` impl DocBuilder { + const SRC: &'static str = "src"; + const SOL_EXT: &'static str = "sol"; const README: &'static str = "README.md"; const SUMMARY: &'static str = "SUMMARY.md"; - const SOL_EXT: &'static str = "sol"; + + /// Create new instance of builder. + pub fn new(root: PathBuf, sources: PathBuf) -> Self { + Self { + root, + sources, + out: Default::default(), + title: Default::default(), + preprocessors: Default::default(), + } + } + + /// Set an `out` path on the builder. + pub fn with_out(mut self, out: PathBuf) -> Self { + self.out = out; + self + } + + /// Set title on the builder + pub fn with_title(mut self, title: String) -> Self { + self.title = title; + self + } + + /// Set preprocessors on the builder. + pub fn with_preprocessors(mut self, preprocessors: Vec>) -> Self { + self.preprocessors = preprocessors; + self + } /// Get the output directory pub fn out_dir(&self) -> PathBuf { @@ -60,7 +73,7 @@ impl DocBuilder { pub fn build(self) -> eyre::Result<()> { // Collect and parse source files let sources: Vec<_> = source_files_iter(&self.sources).collect(); - let files = sources + let documents = sources .par_iter() .enumerate() .map(|(i, path)| { @@ -79,30 +92,33 @@ impl DocBuilder { .into_iter() .map(|item| { let relative_path = path.strip_prefix(&self.root)?.join(item.filename()); - let target_path = self.out.join("src").join(relative_path); - Ok(DocFile::new(item, path.clone(), target_path)) + let target_path = self.out.join(Self::SRC).join(relative_path); + Ok(Document::new(item, path.clone(), target_path)) }) .collect::>>() }) .collect::>>()?; // Flatten and sort the results - let files = files.into_iter().flatten().sorted_by(|file1, file2| { - file1.item_path.display().to_string().cmp(&file2.item_path.display().to_string()) + let documents = documents.into_iter().flatten().sorted_by(|doc1, doc2| { + doc1.item_path.display().to_string().cmp(&doc2.item_path.display().to_string()) }); // Apply preprocessors to files - // TODO: + let documents = self + .preprocessors + .iter() + .try_fold(documents.collect::>(), |docs, p| p.preprocess(docs))?; // Write mdbook related files - self.write_mdbook(files.collect::>())?; + self.write_mdbook(documents)?; Ok(()) } - fn write_mdbook(&self, files: Vec) -> eyre::Result<()> { + fn write_mdbook(&self, documents: Vec) -> eyre::Result<()> { let out_dir = self.out_dir(); - let out_dir_src = out_dir.join("src"); + let out_dir_src = out_dir.join(Self::SRC); fs::create_dir_all(&out_dir_src)?; // Write readme content if any @@ -121,7 +137,7 @@ impl DocBuilder { let mut summary = BufWriter::default(); summary.write_title("Summary")?; summary.write_link_list_item("README", Self::README, 0)?; - self.write_summary_section(&mut summary, &files.iter().collect::>(), None, 0)?; + self.write_summary_section(&mut summary, &documents.iter().collect::>(), None, 0)?; fs::write(out_dir_src.join(Self::SUMMARY), summary.finish())?; // Create dir for static files @@ -145,12 +161,14 @@ impl DocBuilder { fs::write(self.out_dir().join(".gitignore"), gitignore)?; // Write doc files - for file in files { - let doc_content = file.item.as_doc()?; + for document in documents { fs::create_dir_all( - file.target_path.parent().ok_or(eyre::format_err!("empty target path; noop"))?, + document + .target_path + .parent() + .ok_or(eyre::format_err!("empty target path; noop"))?, )?; - fs::write(file.target_path, doc_content)?; + fs::write(&document.target_path, document.as_doc()?)?; } Ok(()) @@ -159,15 +177,15 @@ impl DocBuilder { fn write_summary_section( &self, summary: &mut BufWriter, - files: &[&DocFile], - path: Option<&Path>, + files: &[&Document], + base_path: Option<&Path>, depth: usize, ) -> eyre::Result<()> { if files.is_empty() { return Ok(()) } - if let Some(path) = path { + if let Some(path) = base_path { let title = path.iter().last().unwrap().to_string_lossy(); if depth == 1 { summary.write_title(&title)?; @@ -207,13 +225,34 @@ impl DocBuilder { if path.extension().map(|ext| ext.eq(Self::SOL_EXT)).unwrap_or_default() { for file in files { let ident = file.item.source.ident(); + // let mut link_path = file.target_path.strip_prefix("docs/src")?; + // if let Some(path) = base_path { + // link_path = link_path.strip_prefix(path.parent().unwrap())?; + // } + // let link_path = link_path.display().to_string(); + // TODO: + // println!("LINK PATH {link_path}"); + // println!("PATH {:?}", path.display().to_string()); + + let summary_path = file.target_path.strip_prefix("docs/src")?; + summary.write_link_list_item( + &ident, + &summary_path.display().to_string(), + depth, + )?; + + let readme_path = base_path + .map(|path| summary_path.strip_prefix(path)) + .transpose()? + .unwrap_or(summary_path); + readme.write_link_list_item(&ident, &readme_path.display().to_string(), 0)?; - let readme_path = Path::new("/").join(&path).display().to_string(); - readme.write_link_list_item(&ident, &readme_path, 0)?; + println!("SUMMARY PATH {}", summary_path.display().to_string()); + println!("README PATH {}", readme_path.display().to_string()); - let summary_path = - file.target_path.strip_prefix("docs/src")?.display().to_string(); - summary.write_link_list_item(&ident, &summary_path, depth)?; + // let summary_path = + // file.target_path.strip_prefix("docs/src")?.display().to_string(); + // summary.write_link_list_item(&ident, &link_path, depth)?; } } else { let name = path.iter().last().unwrap().to_string_lossy(); @@ -223,39 +262,12 @@ impl DocBuilder { } } if !readme.is_empty() { - if let Some(path) = path { - let path = self.out_dir().join("src").join(path); + if let Some(path) = base_path { + let path = self.out_dir().join(Self::SRC).join(path); fs::create_dir_all(&path)?; fs::write(path.join(Self::README), readme.finish())?; } } Ok(()) } - - // TODO: - fn lookup_contract_base<'a>( - &self, - docs: &[(PathBuf, Vec)], - base: &Base, - ) -> eyre::Result> { - for (base_path, base_doc) in docs { - for base_part in base_doc.iter() { - if let ParseSource::Contract(base_contract) = &base_part.source { - if base.name.identifiers.last().unwrap().name == base_contract.name.name { - let path = PathBuf::from("/").join( - base_path - .strip_prefix(&self.root)? - .join(format!("contract.{}.md", base_contract.name)), - ); - return Ok(Some( - DocOutput::Link(&base.as_doc()?, &path.display().to_string()) - .as_doc()?, - )) - } - } - } - } - - Ok(None) - } } diff --git a/doc/src/document.rs b/doc/src/document.rs new file mode 100644 index 000000000000..9d3f31456ffa --- /dev/null +++ b/doc/src/document.rs @@ -0,0 +1,50 @@ +use derive_more::Deref; +use std::{collections::HashMap, path::PathBuf, sync::Mutex}; + +use crate::{ParseItem, PreprocessorId, PreprocessorOutput}; + +/// The wrapper around the [ParseItem] containing additional +/// information the original item and extra context for outputting it. +#[derive(Debug, Deref)] +pub struct Document { + /// The underlying parsed item. + #[deref] + pub item: ParseItem, + /// The original item path. + pub item_path: PathBuf, + /// The target path where the document will be written. + pub target_path: PathBuf, + /// The preprocessors results. + context: Mutex>, +} + +impl Document { + /// Create new instance of [Document]. + pub fn new(item: ParseItem, item_path: PathBuf, target_path: PathBuf) -> Self { + Self { item, item_path, target_path, context: Mutex::new(HashMap::default()) } + } + + /// Add a preprocessor result to inner document context. + pub fn add_context(&self, id: PreprocessorId, output: PreprocessorOutput) { + let mut context = self.context.lock().expect("failed to lock context"); + context.insert(id, output); + } + + /// Read preprocessor result from context + pub fn get_from_context(&self, id: PreprocessorId) -> Option { + let context = self.context.lock().expect("failed to lock context"); + context.get(&id).cloned() + } +} + +/// TODO: docs +macro_rules! read_context { + ($doc: expr, $id: expr, $variant: ident) => { + $doc.get_from_context($id).map(|out| match out { + // Only a single variant is matched. Otherwise the code is invalid. + PreprocessorOutput::$variant(inner) => inner, + }) + }; +} + +pub(crate) use read_context; diff --git a/doc/src/format/mod.rs b/doc/src/format/mod.rs deleted file mode 100644 index 8df00401aa8f..000000000000 --- a/doc/src/format/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! The module for formatting various parse tree items - -mod as_code; -mod as_doc; - -pub use as_code::AsCode; -pub use as_doc::{AsDoc, AsDocResult}; diff --git a/doc/src/lib.rs b/doc/src/lib.rs index ebb12502a0c2..ac2f1ddfe3a4 100644 --- a/doc/src/lib.rs +++ b/doc/src/lib.rs @@ -10,17 +10,22 @@ //! See [DocBuilder] mod builder; -mod format; -mod helpers; -mod output; +mod document; mod parser; +mod preprocessor; mod writer; /// The documentation builder. pub use builder::DocBuilder; +/// The document output. +pub use document::Document; + /// Solidity parser and related output items. pub use parser::{error, ParseItem, ParseSource, Parser}; -/// Traits for formatting items into doc output/ -pub use format::{AsCode, AsDoc, AsDocResult}; +/// Preprocessors. +pub use preprocessor::*; + +/// Traits for formatting items into doc output. +pub use writer::{AsCode, AsDoc, AsDocResult, BufWriter, Markdown}; diff --git a/doc/src/parser/item.rs b/doc/src/parser/item.rs index 89b1261d2530..db8ab7f99243 100644 --- a/doc/src/parser/item.rs +++ b/doc/src/parser/item.rs @@ -17,8 +17,8 @@ pub struct ParseItem { pub children: Vec, } -/// Filters [ParseItem]'s children and returns the source pt token of the children -/// matching the target variant as well as its comments. +/// Defines a method that filters [ParseItem]'s children and returns the source pt token of the +/// children matching the target variant as well as its comments. /// Returns [Option::None] if no children matching the variant are found. macro_rules! filter_children_fn { ($vis:vis fn $name:ident(&self, $variant:ident) -> $ret:ty) => { @@ -38,6 +38,21 @@ macro_rules! filter_children_fn { }; } +/// Defines a method that returns [ParseSource] inner element if it matches +/// the variant +macro_rules! as_inner_source { + ($vis:vis fn $name:ident(&self, $variant:ident) -> $ret:ty) => { + /// Return inner element if it matches $variant. + /// If the element doesn't match, returns [None] + $vis fn $name(&self) -> Option<&$ret> { + match self.source { + ParseSource::$variant(ref inner) => Some(inner), + _ => None + } + } + }; +} + impl ParseItem { /// Create new instance of [ParseItem]. pub fn new(source: ParseSource) -> Self { @@ -50,6 +65,11 @@ impl ParseItem { self } + /// Return item comments + pub fn comments(&self) -> &Vec { + &self.comments + } + /// Format the item's filename. pub fn filename(&self) -> String { let prefix = match self.source { @@ -71,6 +91,8 @@ impl ParseItem { filter_children_fn!(pub fn errors(&self, Error) -> ErrorDefinition); filter_children_fn!(pub fn structs(&self, Struct) -> StructDefinition); filter_children_fn!(pub fn enums(&self, Enum) -> EnumDefinition); + + as_inner_source!(pub fn as_contract(&self, Contract) -> ContractDefinition); } /// A wrapper type around pt token. diff --git a/doc/src/preprocessor/contract_inheritance.rs b/doc/src/preprocessor/contract_inheritance.rs new file mode 100644 index 000000000000..cca978bae7f3 --- /dev/null +++ b/doc/src/preprocessor/contract_inheritance.rs @@ -0,0 +1,55 @@ +use super::{Preprocessor, PreprocessorId}; +use crate::{Document, PreprocessorOutput}; +use std::{collections::HashMap, path::PathBuf}; + +/// [ContractInheritance] preprocessor id. +pub const CONTRACT_INHERITANCE_ID: PreprocessorId = PreprocessorId("contract_inheritance"); + +/// The contract inheritance preprocessor. +/// It matches the documents with inner [`ParseSource::Contract`](crate::ParseSource) elements, +/// iterates over their [Base](solang_parser::pt::Base)s and attempts +/// to link them with the paths of the other contract documents. +#[derive(Debug)] +pub struct ContractInheritance; + +impl Preprocessor for ContractInheritance { + fn id(&self) -> PreprocessorId { + CONTRACT_INHERITANCE_ID + } + + fn preprocess(&self, documents: Vec) -> Result, eyre::Error> { + for document in documents.iter() { + if let Some(contract) = document.as_contract() { + let mut links = HashMap::default(); + + // Attempt to match bases to other contracts + for base in contract.base.iter() { + let base_ident = base.name.identifiers.last().unwrap().name.clone(); + if let Some(linked) = self.try_link_base(&base_ident, &documents) { + links.insert(base_ident, linked); + } + } + + if !links.is_empty() { + // Write to context + document.add_context(self.id(), PreprocessorOutput::ContractInheritance(links)); + } + } + } + + Ok(documents) + } +} + +impl ContractInheritance { + fn try_link_base<'a>(&self, base: &str, documents: &Vec) -> Option { + for candidate in documents { + if let Some(contract) = candidate.as_contract() { + if base == contract.name.name { + return Some(candidate.target_path.clone()) + } + } + } + None + } +} diff --git a/doc/src/preprocessor/mod.rs b/doc/src/preprocessor/mod.rs new file mode 100644 index 000000000000..965dd5694765 --- /dev/null +++ b/doc/src/preprocessor/mod.rs @@ -0,0 +1,38 @@ +//! Module containing documentation preprocessors. + +mod contract_inheritance; +use std::{collections::HashMap, fmt::Debug, path::PathBuf}; + +pub use contract_inheritance::{ContractInheritance, CONTRACT_INHERITANCE_ID}; + +use crate::Document; + +/// The preprocessor id. +#[derive(Debug, Eq, Hash, PartialEq)] +pub struct PreprocessorId(&'static str); + +/// Preprocessor output. +/// Wraps all exisiting preprocessor outputs +/// in a single abstraction. +#[derive(Debug, Clone)] +pub enum PreprocessorOutput { + /// The contract inheritance output. + /// The map of contract base idents to the path of the base contract. + ContractInheritance(HashMap), +} + +/// Trait for preprocessing and/or modifying existing documents +/// before writing the to disk. +pub trait Preprocessor: Debug { + // TODO: + /// Preprocessor must specify the output type it's writing to the + /// document context. This is the inner type in [PreprocessorOutput] variants. + // type Output = (); + + /// The id of the preprocessor. + /// Used to write data to document context. + fn id(&self) -> PreprocessorId; + + /// Preprocess the collection of documents + fn preprocess(&self, documents: Vec) -> Result, eyre::Error>; +} diff --git a/doc/src/format/as_code.rs b/doc/src/writer/as_code.rs similarity index 100% rename from doc/src/format/as_code.rs rename to doc/src/writer/as_code.rs diff --git a/doc/src/format/as_doc.rs b/doc/src/writer/as_doc.rs similarity index 80% rename from doc/src/format/as_doc.rs rename to doc/src/writer/as_doc.rs index 91327bc112bd..02f0dc5b54b6 100644 --- a/doc/src/format/as_doc.rs +++ b/doc/src/writer/as_doc.rs @@ -1,11 +1,12 @@ use crate::{ - helpers::{comments_by_tag, exclude_comment_tags}, - parser::{ParseItem, ParseSource}, - writer::BufWriter, + document::read_context, parser::ParseSource, writer::BufWriter, Document, Markdown, + PreprocessorOutput, CONTRACT_INHERITANCE_ID, }; use itertools::Itertools; use solang_parser::{doccomment::DocCommentTag, pt::Base}; +use super::helpers::{comments_by_tag, exclude_comment_tags}; + /// The result of [Asdoc::as_doc] method. pub type AsDocResult = Result; @@ -50,29 +51,43 @@ impl AsDoc for Base { } } -impl AsDoc for ParseItem { +impl AsDoc for Document { fn as_doc(&self) -> AsDocResult { let mut writer = BufWriter::default(); - match &self.source { + match &self.item.source { ParseSource::Contract(contract) => { writer.write_title(&contract.name.name)?; if !contract.base.is_empty() { - // TODO: - // Ok(self.lookup_contract_base(docs.as_ref(), base)?.unwrap_or(base.doc())) - // TODO: should be a name & perform lookup - let bases = contract - .base - .iter() - .map(|base| base.as_doc()) - .collect::, _>>()?; writer.write_bold("Inherits:")?; + + let mut bases = vec![]; + let linked = read_context!(self, CONTRACT_INHERITANCE_ID, ContractInheritance); + for base in contract.base.iter() { + let base_doc = base.as_doc()?; + let base_ident = &base.name.identifiers.last().unwrap().name; + bases.push( + linked + .as_ref() + .and_then(|l| { + l.get(base_ident).map(|path| { + Markdown::Link(&base_doc, &path.display().to_string()) + .as_doc() + }) + }) + .transpose()? + .unwrap_or(base_doc), + ) + } + writer.write_raw(bases.join(", "))?; writer.writeln()?; } + // TODO: writeln_raw writer.write_raw(self.comments.as_doc()?)?; + writer.writeln()?; if let Some(state_vars) = self.variables() { writer.write_subtitle("State Variables")?; @@ -94,6 +109,7 @@ impl AsDoc for ParseItem { writer.write_raw( exclude_comment_tags(comments, vec!["param", "return"]).as_doc()?, )?; + writer.writeln()?; // Write function header writer.write_code(func)?; @@ -166,23 +182,23 @@ impl AsDoc for ParseItem { } ParseSource::Variable(var) => { writer.write_title(&var.name.name)?; - writer.write_section(var, &self.comments)?; + writer.write_section(var, self.comments())?; } ParseSource::Event(event) => { writer.write_title(&event.name.name)?; - writer.write_section(event, &self.comments)?; + writer.write_section(event, self.comments())?; } ParseSource::Error(error) => { writer.write_title(&error.name.name)?; - writer.write_section(error, &self.comments)?; + writer.write_section(error, self.comments())?; } ParseSource::Struct(structure) => { writer.write_title(&structure.name.name)?; - writer.write_section(structure, &self.comments)?; + writer.write_section(structure, self.comments())?; } ParseSource::Enum(enumerable) => { writer.write_title(&enumerable.name.name)?; - writer.write_section(enumerable, &self.comments)?; + writer.write_section(enumerable, self.comments())?; } ParseSource::Function(func) => { // TODO: cleanup @@ -194,15 +210,16 @@ impl AsDoc for ParseItem { // Write function docs writer.write_raw( - exclude_comment_tags(&self.comments, vec!["param", "return"]).as_doc()?, + exclude_comment_tags(self.comments(), vec!["param", "return"]).as_doc()?, )?; + writer.writeln()?; // Write function header writer.write_code(func)?; // Write function parameter comments in a table let params = func.params.iter().filter_map(|p| p.1.as_ref()).collect::>(); - let param_comments = comments_by_tag(&self.comments, "param"); + let param_comments = comments_by_tag(self.comments(), "param"); if !params.is_empty() && !param_comments.is_empty() { writer.write_heading("Parameters")?; writer.writeln()?; @@ -215,7 +232,7 @@ impl AsDoc for ParseItem { // Write function parameter comments in a table let returns = func.returns.iter().filter_map(|p| p.1.as_ref()).collect::>(); - let returns_comments = comments_by_tag(&self.comments, "return"); + let returns_comments = comments_by_tag(self.comments(), "return"); if !returns.is_empty() && !returns_comments.is_empty() { writer.write_heading("Returns")?; writer.writeln()?; diff --git a/doc/src/helpers.rs b/doc/src/writer/helpers.rs similarity index 100% rename from doc/src/helpers.rs rename to doc/src/writer/helpers.rs diff --git a/doc/src/output.rs b/doc/src/writer/markdown.rs similarity index 69% rename from doc/src/output.rs rename to doc/src/writer/markdown.rs index d521ff445aaa..2e0d6f9d2a40 100644 --- a/doc/src/output.rs +++ b/doc/src/writer/markdown.rs @@ -1,16 +1,23 @@ -use crate::format::{AsDoc, AsDocResult}; +use crate::{AsDoc, AsDocResult}; -/// TODO: rename -pub(crate) enum DocOutput<'a> { +/// The markdown format. +#[derive(Debug)] +pub enum Markdown<'a> { + /// H1 heading item. H1(&'a str), + /// H2 heading item. H2(&'a str), + /// H3 heading item. H3(&'a str), + /// Bold item. Bold(&'a str), + /// Link item. Link(&'a str, &'a str), + /// Code block item. CodeBlock(&'a str, &'a str), } -impl<'a> AsDoc for DocOutput<'a> { +impl<'a> AsDoc for Markdown<'a> { fn as_doc(&self) -> AsDocResult { let doc = match self { Self::H1(val) => format!("# {val}"), @@ -24,7 +31,7 @@ impl<'a> AsDoc for DocOutput<'a> { } } -impl<'a> std::fmt::Display for DocOutput<'a> { +impl<'a> std::fmt::Display for Markdown<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_fmt(format_args!("{}", self.as_doc()?)) } diff --git a/doc/src/writer/mod.rs b/doc/src/writer/mod.rs new file mode 100644 index 000000000000..a5b189c03ce2 --- /dev/null +++ b/doc/src/writer/mod.rs @@ -0,0 +1,12 @@ +//! The module for writing and formatting various parse tree items. + +mod as_code; +mod as_doc; +mod helpers; +mod markdown; +mod writer; + +pub use as_code::AsCode; +pub use as_doc::{AsDoc, AsDocResult}; +pub use markdown::Markdown; +pub use writer::BufWriter; diff --git a/doc/src/writer.rs b/doc/src/writer/writer.rs similarity index 83% rename from doc/src/writer.rs rename to doc/src/writer/writer.rs index 0767e193c78d..472026fd8820 100644 --- a/doc/src/writer.rs +++ b/doc/src/writer/writer.rs @@ -2,14 +2,12 @@ use itertools::Itertools; use solang_parser::{doccomment::DocCommentTag, pt::Parameter}; use std::fmt::{self, Display, Write}; -use crate::{ - format::{AsCode, AsDoc}, - output::DocOutput, -}; - -/// TODO: comments -#[derive(Default)] -pub(crate) struct BufWriter { +use crate::{AsCode, AsDoc, Markdown}; + +/// The buffered writer. +/// Writes various display items into the internal buffer. +#[derive(Default, Debug)] +pub struct BufWriter { buf: String, } @@ -27,19 +25,19 @@ impl BufWriter { } pub(crate) fn write_title(&mut self, title: &str) -> fmt::Result { - writeln!(self.buf, "{}", DocOutput::H1(title)) + writeln!(self.buf, "{}", Markdown::H1(title)) } pub(crate) fn write_subtitle(&mut self, subtitle: &str) -> fmt::Result { - writeln!(self.buf, "{}", DocOutput::H2(subtitle)) + writeln!(self.buf, "{}", Markdown::H2(subtitle)) } pub(crate) fn write_heading(&mut self, subtitle: &str) -> fmt::Result { - writeln!(self.buf, "{}", DocOutput::H3(subtitle)) + writeln!(self.buf, "{}", Markdown::H3(subtitle)) } pub(crate) fn write_bold(&mut self, text: &str) -> fmt::Result { - writeln!(self.buf, "{}", DocOutput::Bold(text)) + writeln!(self.buf, "{}", Markdown::Bold(text)) } pub(crate) fn write_list_item(&mut self, item: &str, depth: usize) -> fmt::Result { @@ -53,13 +51,13 @@ impl BufWriter { path: &str, depth: usize, ) -> fmt::Result { - let link = DocOutput::Link(name, path); + let link = Markdown::Link(name, path); self.write_list_item(&link.as_doc()?, depth) } pub(crate) fn write_code(&mut self, item: T) -> fmt::Result { let code = item.as_code(); - let block = DocOutput::CodeBlock("solidity", &code); + let block = Markdown::CodeBlock("solidity", &code); writeln!(self.buf, "{block}") } @@ -110,16 +108,21 @@ impl BufWriter { self.write_piped(&row.join("|"))?; } + self.writeln()?; + Ok(()) } pub(crate) fn write_piped(&mut self, content: &str) -> fmt::Result { self.write_raw("|")?; self.write_raw(content)?; - self.write_raw("|") + self.write_raw("|")?; + self.writeln() } pub(crate) fn finish(self) -> String { self.buf } } + +// TODO: tests From 2b6382a321b61e8c939b1becf33965f0a09876d2 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Sun, 8 Jan 2023 15:09:31 +0200 Subject: [PATCH 27/67] comments abstraction, add book.css, refactor and cleanup some code --- doc/src/builder.rs | 30 ++----- doc/src/lib.rs | 4 +- doc/src/parser/comment.rs | 182 ++++++++++++++++++++++++++++++++++++++ doc/src/parser/error.rs | 17 ++-- doc/src/parser/item.rs | 22 ++--- doc/src/parser/mod.rs | 47 +++++----- doc/src/writer/as_doc.rs | 112 ++++++++--------------- doc/src/writer/helpers.rs | 19 ---- doc/src/writer/mod.rs | 1 - doc/src/writer/writer.rs | 145 +++++++++++++++++++----------- doc/static/book.css | 13 +++ doc/static/book.toml | 3 +- 12 files changed, 372 insertions(+), 223 deletions(-) create mode 100644 doc/src/parser/comment.rs delete mode 100644 doc/src/writer/helpers.rs create mode 100644 doc/static/book.css diff --git a/doc/src/builder.rs b/doc/src/builder.rs index 4f3bdf4d1074..db7a352f27c2 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -140,15 +140,11 @@ impl DocBuilder { self.write_summary_section(&mut summary, &documents.iter().collect::>(), None, 0)?; fs::write(out_dir_src.join(Self::SUMMARY), summary.finish())?; - // Create dir for static files - let out_dir_static = out_dir_src.join("static"); - fs::create_dir_all(&out_dir_static)?; - // Write solidity syntax highlighting - fs::write( - out_dir_static.join("solidity.min.js"), - include_str!("../static/solidity.min.js"), - )?; + fs::write(out_dir.join("solidity.min.js"), include_str!("../static/solidity.min.js"))?; + + // Write css files + fs::write(out_dir.join("book.css"), include_str!("../static/book.css"))?; // Write book config let mut book: toml::Value = toml::from_str(include_str!("../static/book.toml"))?; @@ -219,20 +215,11 @@ impl DocBuilder { } }); - let mut readme = BufWriter::default(); - readme.write_raw("\n\n# Contents\n")?; // TODO: + let mut readme = BufWriter::new("\n\n# Contents\n"); for (path, files) in grouped { if path.extension().map(|ext| ext.eq(Self::SOL_EXT)).unwrap_or_default() { for file in files { let ident = file.item.source.ident(); - // let mut link_path = file.target_path.strip_prefix("docs/src")?; - // if let Some(path) = base_path { - // link_path = link_path.strip_prefix(path.parent().unwrap())?; - // } - // let link_path = link_path.display().to_string(); - // TODO: - // println!("LINK PATH {link_path}"); - // println!("PATH {:?}", path.display().to_string()); let summary_path = file.target_path.strip_prefix("docs/src")?; summary.write_link_list_item( @@ -246,13 +233,6 @@ impl DocBuilder { .transpose()? .unwrap_or(summary_path); readme.write_link_list_item(&ident, &readme_path.display().to_string(), 0)?; - - println!("SUMMARY PATH {}", summary_path.display().to_string()); - println!("README PATH {}", readme_path.display().to_string()); - - // let summary_path = - // file.target_path.strip_prefix("docs/src")?.display().to_string(); - // summary.write_link_list_item(&ident, &link_path, depth)?; } } else { let name = path.iter().last().unwrap().to_string_lossy(); diff --git a/doc/src/lib.rs b/doc/src/lib.rs index ac2f1ddfe3a4..221d2793cdcf 100644 --- a/doc/src/lib.rs +++ b/doc/src/lib.rs @@ -22,7 +22,9 @@ pub use builder::DocBuilder; pub use document::Document; /// Solidity parser and related output items. -pub use parser::{error, ParseItem, ParseSource, Parser}; +pub use parser::{ + error, Comment, CommentTag, Comments, CommentsRef, ParseItem, ParseSource, Parser, +}; /// Preprocessors. pub use preprocessor::*; diff --git a/doc/src/parser/comment.rs b/doc/src/parser/comment.rs new file mode 100644 index 000000000000..284510ddad8d --- /dev/null +++ b/doc/src/parser/comment.rs @@ -0,0 +1,182 @@ +use derive_more::Deref; +use solang_parser::doccomment::DocCommentTag; +use std::str::FromStr; + +/// The natspec comment tag explaining the purpose of the comment. +/// See: https://docs.soliditylang.org/en/v0.8.17/natspec-format.html#tags. +#[derive(PartialEq, Clone, Debug)] +pub enum CommentTag { + /// A title that should describe the contract/interface + Title, + /// The name of the author + Author, + /// Explain to an end user what this does + Notice, + /// Explain to a developer any extra details + Dev, + /// Documents a parameter just like in Doxygen (must be followed by parameter name) + Param, + /// Documents the return variables of a contract’s function + Return, + /// Copies all missing tags from the base function (must be followed by the contract name) + Inheritdoc, + /// Custom tag, semantics is application-defined + Custom(String), +} + +impl FromStr for CommentTag { + type Err = eyre::Error; + + fn from_str(s: &str) -> Result { + let trimmed = s.trim(); + let tag = match trimmed { + "title" => CommentTag::Title, + "author" => CommentTag::Author, + "notice" => CommentTag::Notice, + "dev" => CommentTag::Dev, + "param" => CommentTag::Param, + "return" => CommentTag::Return, + "inheritdoc" => CommentTag::Inheritdoc, + _ if trimmed.starts_with("custom:") => { + CommentTag::Custom(trimmed.trim_start_matches("custom:").trim().to_owned()) + } + _ => eyre::bail!("unknown comment tag: {trimmed}"), + }; + Ok(tag) + } +} + +/// The natspec documentation comment. +/// https://docs.soliditylang.org/en/v0.8.17/natspec-format.html +#[derive(PartialEq, Debug)] +pub struct Comment { + /// The doc comment tag. + pub tag: CommentTag, + /// The doc comment value. + pub value: String, +} + +impl Comment { + /// Create new instance of [Comment]. + pub fn new(tag: CommentTag, value: String) -> Self { + Self { tag, value } + } + + /// Split the comment at first word. + /// Useful for [CommentTag::Param] and [CommentTag::Return] comments. + pub fn split_first_word<'a>(&'a self) -> Option<(&'a str, &'a str)> { + self.value.trim_start().split_once(' ') + } + + /// Match the first word of the comment with the expected. + /// Returns [None] if the word doesn't match. + /// Useful for [CommentTag::Param] and [CommentTag::Return] comments. + pub fn match_first_word<'a>(&'a self, expected: &str) -> Option<&'a str> { + self.split_first_word().and_then( + |(word, rest)| { + if word.eq(expected) { + Some(rest) + } else { + None + } + }, + ) + } +} + +impl TryFrom for Comment { + type Error = eyre::Error; + + fn try_from(value: DocCommentTag) -> Result { + let tag = CommentTag::from_str(&value.tag)?; + Ok(Self { tag, value: value.value }) + } +} + +/// The collection of natspec [Comment] items. +#[derive(Deref, PartialEq, Default, Debug)] +pub struct Comments(Vec); + +/// Forward the [Comments] function implementation to the [CommentsRef] +/// reference type. +macro_rules! ref_fn { + ($vis:vis fn $name:ident(&self, $arg:ty)) => { + /// Forward the function implementation to [CommentsRef] reference type. + $vis fn $name<'a>(&'a self, arg: $arg) -> CommentsRef<'a> { + CommentsRef::from(self).$name(arg) + } + }; +} + +impl Comments { + ref_fn!(pub fn include_tag(&self, CommentTag)); + ref_fn!(pub fn include_tags(&self, &[CommentTag])); + ref_fn!(pub fn exclude_tags(&self, &[CommentTag])); +} + +impl TryFrom> for Comments { + type Error = eyre::Error; + + fn try_from(value: Vec) -> Result { + Ok(Self(value.into_iter().map(TryInto::try_into).collect::, _>>()?)) + } +} + +/// The collection of references to natspec [Comment] items. +#[derive(Deref, PartialEq, Default, Debug)] +pub struct CommentsRef<'a>(Vec<&'a Comment>); + +impl<'a> CommentsRef<'a> { + /// Filter a collection of comments and return only those that match a provided tag + pub fn include_tag(&self, tag: CommentTag) -> CommentsRef<'a> { + self.include_tags(&[tag]) + } + + /// Filter a collection of comments and return only those that match provided tags + pub fn include_tags(&self, tags: &[CommentTag]) -> CommentsRef<'a> { + // Cloning only references here + CommentsRef(self.iter().cloned().filter(|c| tags.contains(&c.tag)).collect()) + } + + /// Filter a collection of comments and return only those that do not match provided tags + pub fn exclude_tags(&self, tags: &[CommentTag]) -> CommentsRef<'a> { + // Cloning only references here + CommentsRef(self.iter().cloned().filter(|c| !tags.contains(&c.tag)).collect()) + } +} + +impl<'a> From<&'a Comments> for CommentsRef<'a> { + fn from(value: &'a Comments) -> Self { + Self(value.iter().collect()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_comment_tag() { + assert_eq!(CommentTag::from_str("title").unwrap(), CommentTag::Title); + assert_eq!(CommentTag::from_str(" title ").unwrap(), CommentTag::Title); + assert_eq!(CommentTag::from_str("author").unwrap(), CommentTag::Author); + assert_eq!(CommentTag::from_str("notice").unwrap(), CommentTag::Notice); + assert_eq!(CommentTag::from_str("dev").unwrap(), CommentTag::Dev); + assert_eq!(CommentTag::from_str("param").unwrap(), CommentTag::Param); + assert_eq!(CommentTag::from_str("return").unwrap(), CommentTag::Return); + assert_eq!(CommentTag::from_str("inheritdoc").unwrap(), CommentTag::Inheritdoc); + assert_eq!(CommentTag::from_str("custom:").unwrap(), CommentTag::Custom("".to_owned())); + assert_eq!( + CommentTag::from_str("custom:some").unwrap(), + CommentTag::Custom("some".to_owned()) + ); + assert_eq!( + CommentTag::from_str(" custom: some ").unwrap(), + CommentTag::Custom("some".to_owned()) + ); + + assert!(CommentTag::from_str("").is_err()); + assert!(CommentTag::from_str("custom").is_err()); + assert!(CommentTag::from_str("sometag").is_err()); + } +} diff --git a/doc/src/parser/error.rs b/doc/src/parser/error.rs index fe9f09c854fa..ad39f0dc1807 100644 --- a/doc/src/parser/error.rs +++ b/doc/src/parser/error.rs @@ -1,16 +1,9 @@ -use std::fmt::{Display, Formatter}; - use thiserror::Error; -/// The parser error +/// The parser error. #[derive(Error, Debug)] -pub struct NoopError; - -impl Display for NoopError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str("noop;") - } -} +#[error(transparent)] +pub struct ParserError(#[from] eyre::Error); -/// A result with default noop error -pub type ParserResult = std::result::Result; +/// The parser result. +pub type ParserResult = std::result::Result; diff --git a/doc/src/parser/item.rs b/doc/src/parser/item.rs index db8ab7f99243..6b0844e5c0f0 100644 --- a/doc/src/parser/item.rs +++ b/doc/src/parser/item.rs @@ -1,18 +1,17 @@ -use solang_parser::{ - doccomment::DocCommentTag, - pt::{ - ContractDefinition, EnumDefinition, ErrorDefinition, EventDefinition, FunctionDefinition, - StructDefinition, VariableDefinition, - }, +use solang_parser::pt::{ + ContractDefinition, EnumDefinition, ErrorDefinition, EventDefinition, FunctionDefinition, + StructDefinition, VariableDefinition, }; +use crate::Comments; + /// The parsed item. #[derive(Debug, PartialEq)] pub struct ParseItem { /// The parse tree source. pub source: ParseSource, /// Item comments. - pub comments: Vec, + pub comments: Comments, /// Children items. pub children: Vec, } @@ -23,7 +22,7 @@ pub struct ParseItem { macro_rules! filter_children_fn { ($vis:vis fn $name:ident(&self, $variant:ident) -> $ret:ty) => { /// Filter children items for [ParseSource::$variant] variants. - $vis fn $name<'a>(&'a self) -> Option)>> { + $vis fn $name<'a>(&'a self) -> Option> { let items = self.children.iter().filter_map(|item| match item.source { ParseSource::$variant(ref inner) => Some((inner, &item.comments)), _ => None, @@ -60,16 +59,11 @@ impl ParseItem { } /// Set comments on the [ParseItem]. - pub fn with_comments(mut self, comments: Vec) -> Self { + pub fn with_comments(mut self, comments: Comments) -> Self { self.comments = comments; self } - /// Return item comments - pub fn comments(&self) -> &Vec { - &self.comments - } - /// Format the item's filename. pub fn filename(&self) -> String { let prefix = match self.source { diff --git a/doc/src/parser/mod.rs b/doc/src/parser/mod.rs index 65a372409f02..13acd8762605 100644 --- a/doc/src/parser/mod.rs +++ b/doc/src/parser/mod.rs @@ -2,27 +2,31 @@ use forge_fmt::{Visitable, Visitor}; use solang_parser::{ - doccomment::{parse_doccomments, DocComment, DocCommentTag}, + doccomment::{parse_doccomments, DocComment}, pt::{ - Comment, EnumDefinition, ErrorDefinition, EventDefinition, FunctionDefinition, Loc, - SourceUnit, SourceUnitPart, StructDefinition, VariableDefinition, + Comment as SolangComment, EnumDefinition, ErrorDefinition, EventDefinition, + FunctionDefinition, Loc, SourceUnit, SourceUnitPart, StructDefinition, VariableDefinition, }, }; /// Parser error. pub mod error; -use error::{NoopError, ParserResult}; +use error::{ParserError, ParserResult}; mod item; pub use item::{ParseItem, ParseSource}; +/// Doc comment. +mod comment; +pub use comment::{Comment, CommentTag, Comments, CommentsRef}; + /// The documentation parser. This type implements a [Visitor] trait. While walking the parse tree, /// [Parser] will collect relevant source items and corresponding doc comments. The resulting /// [ParseItem]s can be accessed by calling [Parser::items]. #[derive(Debug, Default)] pub struct Parser { /// Initial comments from solang parser. - comments: Vec, + comments: Vec, /// Parser context. context: ParserContext, /// Parsed results. @@ -40,7 +44,7 @@ struct ParserContext { impl Parser { /// Create a new instance of [Parser]. - pub fn new(comments: Vec) -> Self { + pub fn new(comments: Vec) -> Self { Parser { comments, ..Default::default() } } @@ -69,18 +73,19 @@ impl Parser { /// Adds a child element to the parent item if it exists. /// Otherwise the element will be added to a top-level items collection. /// Moves the doc comment pointer to the end location of the child element. - fn add_element_to_parent(&mut self, source: ParseSource, loc: Loc) { - let child = ParseItem { source, comments: self.parse_docs(loc.start()), children: vec![] }; + fn add_element_to_parent(&mut self, source: ParseSource, loc: Loc) -> ParserResult<()> { + let child = ParseItem { source, comments: self.parse_docs(loc.start())?, children: vec![] }; if let Some(parent) = self.context.parent.as_mut() { parent.children.push(child); } else { self.items.push(child); } self.context.doc_start_loc = loc.end(); + Ok(()) } /// Parse the doc comments from the current start location. - fn parse_docs(&mut self, end: usize) -> Vec { + fn parse_docs(&mut self, end: usize) -> ParserResult { let mut res = vec![]; for comment in parse_doccomments(&self.comments, self.context.doc_start_loc, end) { match comment { @@ -88,12 +93,12 @@ impl Parser { DocComment::Block { comments } => res.extend(comments.into_iter()), } } - res + Ok(res.try_into()?) } } impl Visitor for Parser { - type Error = NoopError; + type Error = ParserError; fn visit_source_unit(&mut self, source_unit: &mut SourceUnit) -> ParserResult<()> { for source in source_unit.0.iter_mut() { @@ -101,7 +106,7 @@ impl Visitor for Parser { SourceUnitPart::ContractDefinition(def) => { // Create new contract parse item. let source = ParseSource::Contract(def.clone()); - let comments = self.parse_docs(def.loc.start()); + let comments = self.parse_docs(def.loc.start())?; let contract = ParseItem::new(source).with_comments(comments); // Move the doc pointer to the contract location start. @@ -136,33 +141,27 @@ impl Visitor for Parser { } fn visit_function(&mut self, func: &mut FunctionDefinition) -> ParserResult<()> { - self.add_element_to_parent(ParseSource::Function(func.clone()), func.loc); - Ok(()) + self.add_element_to_parent(ParseSource::Function(func.clone()), func.loc) } fn visit_var_definition(&mut self, var: &mut VariableDefinition) -> ParserResult<()> { - self.add_element_to_parent(ParseSource::Variable(var.clone()), var.loc); - Ok(()) + self.add_element_to_parent(ParseSource::Variable(var.clone()), var.loc) } fn visit_event(&mut self, event: &mut EventDefinition) -> ParserResult<()> { - self.add_element_to_parent(ParseSource::Event(event.clone()), event.loc); - Ok(()) + self.add_element_to_parent(ParseSource::Event(event.clone()), event.loc) } fn visit_error(&mut self, error: &mut ErrorDefinition) -> ParserResult<()> { - self.add_element_to_parent(ParseSource::Error(error.clone()), error.loc); - Ok(()) + self.add_element_to_parent(ParseSource::Error(error.clone()), error.loc) } fn visit_struct(&mut self, structure: &mut StructDefinition) -> ParserResult<()> { - self.add_element_to_parent(ParseSource::Struct(structure.clone()), structure.loc); - Ok(()) + self.add_element_to_parent(ParseSource::Struct(structure.clone()), structure.loc) } fn visit_enum(&mut self, enumerable: &mut EnumDefinition) -> ParserResult<()> { - self.add_element_to_parent(ParseSource::Enum(enumerable.clone()), enumerable.loc); - Ok(()) + self.add_element_to_parent(ParseSource::Enum(enumerable.clone()), enumerable.loc) } } diff --git a/doc/src/writer/as_doc.rs b/doc/src/writer/as_doc.rs index 02f0dc5b54b6..28630fccb1ee 100644 --- a/doc/src/writer/as_doc.rs +++ b/doc/src/writer/as_doc.rs @@ -1,11 +1,9 @@ use crate::{ - document::read_context, parser::ParseSource, writer::BufWriter, Document, Markdown, - PreprocessorOutput, CONTRACT_INHERITANCE_ID, + document::read_context, parser::ParseSource, writer::BufWriter, CommentTag, Comments, + CommentsRef, Document, Markdown, PreprocessorOutput, CONTRACT_INHERITANCE_ID, }; use itertools::Itertools; -use solang_parser::{doccomment::DocCommentTag, pt::Base}; - -use super::helpers::{comments_by_tag, exclude_comment_tags}; +use solang_parser::pt::Base; /// The result of [Asdoc::as_doc] method. pub type AsDocResult = Result; @@ -23,25 +21,33 @@ impl AsDoc for String { } } -impl AsDoc for DocCommentTag { +impl AsDoc for Comments { fn as_doc(&self) -> AsDocResult { - Ok(self.value.to_owned()) + CommentsRef::from(self).as_doc() } } -impl AsDoc for Vec<&DocCommentTag> { +impl<'a> AsDoc for CommentsRef<'a> { fn as_doc(&self) -> AsDocResult { - Ok(self - .iter() - .map(|c| DocCommentTag::as_doc(*c)) - .collect::, _>>()? - .join("\n\n")) - } -} + let mut writer = BufWriter::default(); -impl AsDoc for Vec { - fn as_doc(&self) -> AsDocResult { - Ok(self.iter().map(DocCommentTag::as_doc).collect::, _>>()?.join("\n\n")) + // TODO: title + + let authors = self.include_tag(CommentTag::Author); + if !authors.is_empty() { + writer.write_bold(&format!("Author{}:", if authors.len() == 1 { "" } else { "s" }))?; + writer.writeln_raw(authors.iter().map(|a| &a.value).join(", "))?; + writer.writeln()?; + } + + // TODO: other tags + let docs = self.include_tags(&[CommentTag::Dev, CommentTag::Notice]); + for doc in docs.iter() { + writer.writeln_raw(&doc.value)?; + writer.writeln()?; + } + + Ok(writer.finish()) } } @@ -81,13 +87,11 @@ impl AsDoc for Document { ) } - writer.write_raw(bases.join(", "))?; + writer.writeln_raw(bases.join(", "))?; writer.writeln()?; } - // TODO: writeln_raw - writer.write_raw(self.comments.as_doc()?)?; - writer.writeln()?; + writer.writeln_doc(&self.comments)?; if let Some(state_vars) = self.variables() { writer.write_subtitle("State Variables")?; @@ -106,10 +110,9 @@ impl AsDoc for Document { writer.writeln()?; // Write function docs - writer.write_raw( - exclude_comment_tags(comments, vec!["param", "return"]).as_doc()?, + writer.writeln_doc( + comments.exclude_tags(&[CommentTag::Param, CommentTag::Return]), )?; - writer.writeln()?; // Write function header writer.write_code(func)?; @@ -117,30 +120,12 @@ impl AsDoc for Document { // Write function parameter comments in a table let params = func.params.iter().filter_map(|p| p.1.as_ref()).collect::>(); - let param_comments = comments_by_tag(comments, "param"); - if !params.is_empty() && !param_comments.is_empty() { - writer.write_heading("Parameters")?; - writer.writeln()?; - writer.write_param_table( - &["Name", "Type", "Description"], - ¶ms, - ¶m_comments, - )? - } + writer.try_write_param_table(CommentTag::Param, ¶ms, comments)?; // Write function parameter comments in a table let returns = func.returns.iter().filter_map(|p| p.1.as_ref()).collect::>(); - let returns_comments = comments_by_tag(comments, "return"); - if !returns.is_empty() && !returns_comments.is_empty() { - writer.write_heading("Returns")?; - writer.writeln()?; - writer.write_param_table( - &["Name", "Type", "Description"], - &returns, - &returns_comments, - )?; - } + writer.try_write_param_table(CommentTag::Return, &returns, comments)?; writer.writeln()?; @@ -182,23 +167,23 @@ impl AsDoc for Document { } ParseSource::Variable(var) => { writer.write_title(&var.name.name)?; - writer.write_section(var, self.comments())?; + writer.write_section(var, &self.comments)?; } ParseSource::Event(event) => { writer.write_title(&event.name.name)?; - writer.write_section(event, self.comments())?; + writer.write_section(event, &self.comments)?; } ParseSource::Error(error) => { writer.write_title(&error.name.name)?; - writer.write_section(error, self.comments())?; + writer.write_section(error, &self.comments)?; } ParseSource::Struct(structure) => { writer.write_title(&structure.name.name)?; - writer.write_section(structure, self.comments())?; + writer.write_section(structure, &self.comments)?; } ParseSource::Enum(enumerable) => { writer.write_title(&enumerable.name.name)?; - writer.write_section(enumerable, self.comments())?; + writer.write_section(enumerable, &self.comments)?; } ParseSource::Function(func) => { // TODO: cleanup @@ -209,39 +194,20 @@ impl AsDoc for Document { writer.writeln()?; // Write function docs - writer.write_raw( - exclude_comment_tags(self.comments(), vec!["param", "return"]).as_doc()?, + writer.writeln_doc( + self.comments.exclude_tags(&[CommentTag::Param, CommentTag::Return]), )?; - writer.writeln()?; // Write function header writer.write_code(func)?; // Write function parameter comments in a table let params = func.params.iter().filter_map(|p| p.1.as_ref()).collect::>(); - let param_comments = comments_by_tag(self.comments(), "param"); - if !params.is_empty() && !param_comments.is_empty() { - writer.write_heading("Parameters")?; - writer.writeln()?; - writer.write_param_table( - &["Name", "Type", "Description"], - ¶ms, - ¶m_comments, - )? - } + writer.try_write_param_table(CommentTag::Param, ¶ms, &self.comments)?; // Write function parameter comments in a table let returns = func.returns.iter().filter_map(|p| p.1.as_ref()).collect::>(); - let returns_comments = comments_by_tag(self.comments(), "return"); - if !returns.is_empty() && !returns_comments.is_empty() { - writer.write_heading("Returns")?; - writer.writeln()?; - writer.write_param_table( - &["Name", "Type", "Description"], - &returns, - &returns_comments, - )?; - } + writer.try_write_param_table(CommentTag::Return, &returns, &self.comments)?; writer.writeln()?; } diff --git a/doc/src/writer/helpers.rs b/doc/src/writer/helpers.rs deleted file mode 100644 index 1261a055097c..000000000000 --- a/doc/src/writer/helpers.rs +++ /dev/null @@ -1,19 +0,0 @@ -use solang_parser::doccomment::DocCommentTag; - -/// Filter a collection of comments and return -/// only those that match a given tag -pub(crate) fn comments_by_tag<'a>( - comments: &'a [DocCommentTag], - tag: &str, -) -> Vec<&'a DocCommentTag> { - comments.iter().filter(|c| c.tag == tag).collect() -} - -/// Filter a collection of comments and return -/// only those that do not have provided tags -pub(crate) fn exclude_comment_tags<'a>( - comments: &'a [DocCommentTag], - tags: Vec<&str>, -) -> Vec<&'a DocCommentTag> { - comments.iter().filter(|c| !tags.contains(&c.tag.as_str())).collect() -} diff --git a/doc/src/writer/mod.rs b/doc/src/writer/mod.rs index a5b189c03ce2..2f909a5de9d2 100644 --- a/doc/src/writer/mod.rs +++ b/doc/src/writer/mod.rs @@ -2,7 +2,6 @@ mod as_code; mod as_doc; -mod helpers; mod markdown; mod writer; diff --git a/doc/src/writer/writer.rs b/doc/src/writer/writer.rs index 472026fd8820..af60ac79a03b 100644 --- a/doc/src/writer/writer.rs +++ b/doc/src/writer/writer.rs @@ -1,8 +1,8 @@ use itertools::Itertools; -use solang_parser::{doccomment::DocCommentTag, pt::Parameter}; +use solang_parser::pt::Parameter; use std::fmt::{self, Display, Write}; -use crate::{AsCode, AsDoc, Markdown}; +use crate::{AsCode, AsDoc, CommentTag, Comments, Markdown}; /// The buffered writer. /// Writes various display items into the internal buffer. @@ -12,98 +12,138 @@ pub struct BufWriter { } impl BufWriter { - pub(crate) fn is_empty(&self) -> bool { + const PARAM_TABLE_HEADERS: &'static [&'static str] = &["Name", "Type", "Description"]; + + /// Create new instance of [BufWriter] from [ToString]. + pub fn new(content: impl ToString) -> Self { + Self { buf: content.to_string() } + } + + /// Returns true if the buffer is empty. + pub fn is_empty(&self) -> bool { self.buf.is_empty() } - pub(crate) fn write_raw(&mut self, content: T) -> fmt::Result { + /// Write [AsDoc] implementation to the buffer. + pub fn write_doc(&mut self, doc: &T) -> fmt::Result { + write!(self.buf, "{}", doc.as_doc()?) + } + + /// Write [AsDoc] implementation to the buffer with newline. + pub fn writeln_doc(&mut self, doc: T) -> fmt::Result { + writeln!(self.buf, "{}", doc.as_doc()?) + } + + /// Writes raw content to the buffer. + pub fn write_raw(&mut self, content: T) -> fmt::Result { write!(self.buf, "{content}") } - pub(crate) fn writeln(&mut self) -> fmt::Result { + /// Writes raw content to the buffer with newline. + pub fn writeln_raw(&mut self, content: T) -> fmt::Result { + writeln!(self.buf, "{content}") + } + + /// Writes newline to the buffer. + pub fn writeln(&mut self) -> fmt::Result { writeln!(self.buf) } - pub(crate) fn write_title(&mut self, title: &str) -> fmt::Result { + /// Writes a title to the buffer formatted as [Markdown::H1]. + pub fn write_title(&mut self, title: &str) -> fmt::Result { writeln!(self.buf, "{}", Markdown::H1(title)) } - pub(crate) fn write_subtitle(&mut self, subtitle: &str) -> fmt::Result { + /// Writes a subtitle to the bugger formatted as [Markdown::H2]. + pub fn write_subtitle(&mut self, subtitle: &str) -> fmt::Result { writeln!(self.buf, "{}", Markdown::H2(subtitle)) } - pub(crate) fn write_heading(&mut self, subtitle: &str) -> fmt::Result { - writeln!(self.buf, "{}", Markdown::H3(subtitle)) + /// Writes heading to the buffer formatted as [Markdown::H3]. + pub fn write_heading(&mut self, heading: &str) -> fmt::Result { + writeln!(self.buf, "{}", Markdown::H3(heading)) } - pub(crate) fn write_bold(&mut self, text: &str) -> fmt::Result { + /// Writes bold text to the bufffer formatted as [Markdown::Bold]. + pub fn write_bold(&mut self, text: &str) -> fmt::Result { writeln!(self.buf, "{}", Markdown::Bold(text)) } - pub(crate) fn write_list_item(&mut self, item: &str, depth: usize) -> fmt::Result { + /// Writes a list item to the bufffer indented by specified depth. + pub fn write_list_item(&mut self, item: &str, depth: usize) -> fmt::Result { let indent = " ".repeat(depth * 2); writeln!(self.buf, "{indent}- {item}") } - pub(crate) fn write_link_list_item( - &mut self, - name: &str, - path: &str, - depth: usize, - ) -> fmt::Result { + /// Writes a link to the buffer as a list item. + pub fn write_link_list_item(&mut self, name: &str, path: &str, depth: usize) -> fmt::Result { let link = Markdown::Link(name, path); self.write_list_item(&link.as_doc()?, depth) } - pub(crate) fn write_code(&mut self, item: T) -> fmt::Result { + /// Writes a solidity code block block to the buffer. + pub fn write_code(&mut self, item: T) -> fmt::Result { let code = item.as_code(); let block = Markdown::CodeBlock("solidity", &code); writeln!(self.buf, "{block}") } - // TODO: revise - pub(crate) fn write_section( - &mut self, - item: T, - comments: &Vec, - ) -> fmt::Result { - self.write_raw(&comments.as_doc()?)?; - self.writeln()?; + /// Write an item section to the buffer. First write comments, the item itself as code. + pub fn write_section(&mut self, item: T, comments: &Comments) -> fmt::Result { + self.writeln_raw(&comments.as_doc()?)?; self.write_code(item)?; - self.writeln()?; - Ok(()) + self.writeln() } - pub(crate) fn write_param_table( + /// Tries to write the parameters table to the buffer. + /// Doesn't write anything if either params or comments are empty. + pub fn try_write_param_table( &mut self, - headers: &[&str], + tag: CommentTag, params: &[&Parameter], - comments: &[&DocCommentTag], + comments: &Comments, ) -> fmt::Result { - self.write_piped(&headers.join("|"))?; + let comments = comments.include_tag(tag.clone()); + + // There is nothing to write. + if params.is_empty() || comments.is_empty() { + return Ok(()) + } + + let heading = match &tag { + CommentTag::Param => "Parameters", + CommentTag::Return => "Returns", + _ => return Err(fmt::Error), + }; + + self.write_heading(heading)?; + self.writeln()?; - let separator = headers.iter().map(|h| "-".repeat(h.len())).join("|"); + self.write_piped(&Self::PARAM_TABLE_HEADERS.join("|"))?; + + // TODO: lazy? + let separator = Self::PARAM_TABLE_HEADERS.iter().map(|h| "-".repeat(h.len())).join("|"); self.write_piped(&separator)?; - for param in params { + for (index, param) in params.into_iter().enumerate() { let param_name = param.name.as_ref().map(|n| n.name.to_owned()); - let description = param_name - .as_ref() - .and_then(|name| { - comments.iter().find_map(|comment| { - match comment.value.trim_start().split_once(' ') { - Some((tag_name, description)) if tag_name.trim().eq(name.as_str()) => { - Some(description.replace('\n', " ")) - } - _ => None, - } - }) + + let mut comment = param_name.as_ref().and_then(|name| { + comments.iter().find_map(|comment| { + comment.match_first_word(name.as_str()).map(|rest| rest.replace('\n', " ")) }) - .unwrap_or_default(); + }); + + // If it's a return tag and couldn't match by first word, + // lookup the doc by index. + if comment.is_none() && matches!(tag, CommentTag::Return) { + comment = comments.get(index).map(|c| c.value.clone()); + } + let row = [ param_name.unwrap_or_else(|| "".to_owned()), param.ty.as_code(), - description, + comment.unwrap_or_default(), ]; self.write_piped(&row.join("|"))?; } @@ -113,16 +153,15 @@ impl BufWriter { Ok(()) } - pub(crate) fn write_piped(&mut self, content: &str) -> fmt::Result { + /// Write content to the buffer surrounded by pipes. + pub fn write_piped(&mut self, content: &str) -> fmt::Result { self.write_raw("|")?; self.write_raw(content)?; - self.write_raw("|")?; - self.writeln() + self.writeln_raw("|") } - pub(crate) fn finish(self) -> String { + /// Finish and return underlying buffer. + pub fn finish(self) -> String { self.buf } } - -// TODO: tests diff --git a/doc/static/book.css b/doc/static/book.css new file mode 100644 index 000000000000..b5ce903f99b7 --- /dev/null +++ b/doc/static/book.css @@ -0,0 +1,13 @@ +table { + margin: 0 auto; + border-collapse: collapse; + width: 100%; +} + +table td:first-child { + width: 15%; +} + +table td:nth-child(2) { + width: 25%; +} \ No newline at end of file diff --git a/doc/static/book.toml b/doc/static/book.toml index d2947a7acef3..f5fe02033526 100644 --- a/doc/static/book.toml +++ b/doc/static/book.toml @@ -3,7 +3,8 @@ src = "src" [output.html] no-section-label = true -additional-js = ["src/static/solidity.min.js"] +additional-js = ["solidity.min.js"] +additional-css = ["book.css"] [output.html.fold] enable = true From 8a78776791c63a3b54c3016762e3938af74e7afa Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Sun, 8 Jan 2023 15:19:13 +0200 Subject: [PATCH 28/67] enable contract inheritance preprocessor --- cli/src/cmd/forge/doc.rs | 4 ++-- doc/src/builder.rs | 4 ++-- doc/src/writer/as_doc.rs | 6 ++++++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/cli/src/cmd/forge/doc.rs b/cli/src/cmd/forge/doc.rs index 6aa4c2f9486f..59eab8fc59b9 100644 --- a/cli/src/cmd/forge/doc.rs +++ b/cli/src/cmd/forge/doc.rs @@ -1,6 +1,6 @@ use crate::cmd::Cmd; use clap::{Parser, ValueHint}; -use forge_doc::DocBuilder; +use forge_doc::{ContractInheritance, DocBuilder}; use foundry_config::{find_project_root_path, load_config_with_root}; use std::path::PathBuf; @@ -36,7 +36,7 @@ impl Cmd for DocArgs { DocBuilder::new(root, config.project_paths().sources) .with_out(self.out.clone().unwrap_or(config.doc.out.clone())) .with_title(config.doc.title.clone()) - // TODO: .with_preprocessors() + .with_preprocessor(ContractInheritance) .build() } } diff --git a/doc/src/builder.rs b/doc/src/builder.rs index db7a352f27c2..55bb43426770 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -59,8 +59,8 @@ impl DocBuilder { } /// Set preprocessors on the builder. - pub fn with_preprocessors(mut self, preprocessors: Vec>) -> Self { - self.preprocessors = preprocessors; + pub fn with_preprocessor(mut self, preprocessor: P) -> Self { + self.preprocessors.push(Box::new(preprocessor) as Box); self } diff --git a/doc/src/writer/as_doc.rs b/doc/src/writer/as_doc.rs index 28630fccb1ee..77d01bcdff73 100644 --- a/doc/src/writer/as_doc.rs +++ b/doc/src/writer/as_doc.rs @@ -1,3 +1,5 @@ +use std::path::Path; + use crate::{ document::read_context, parser::ParseSource, writer::BufWriter, CommentTag, Comments, CommentsRef, Document, Markdown, PreprocessorOutput, CONTRACT_INHERITANCE_ID, @@ -78,6 +80,10 @@ impl AsDoc for Document { .as_ref() .and_then(|l| { l.get(base_ident).map(|path| { + let path = Path::new("/").join( + // TODO: move to func + path.strip_prefix("docs/src").ok().unwrap_or(path), + ); Markdown::Link(&base_doc, &path.display().to_string()) .as_doc() }) From 5cc9a97f32519ab6737a15d536c444b764fbe654 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Sun, 8 Jan 2023 16:30:09 +0200 Subject: [PATCH 29/67] display constant init value --- Cargo.lock | 1 + doc/Cargo.toml | 1 + doc/src/writer/as_code.rs | 33 ++++++++++++++++++++++++++++++++- doc/src/writer/writer.rs | 2 +- 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ce76794328e9..ea93bbce032c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2343,6 +2343,7 @@ dependencies = [ "auto_impl 1.0.1", "clap 3.2.23", "derive_more", + "ethers-core", "ethers-solc", "eyre", "forge-fmt", diff --git a/doc/Cargo.toml b/doc/Cargo.toml index f6ed6db2cd21..7f94fcbb8ed3 100644 --- a/doc/Cargo.toml +++ b/doc/Cargo.toml @@ -15,6 +15,7 @@ forge-fmt = { path = "../fmt" } # ethers ethers-solc = { git = "https://github.com/gakonst/ethers-rs", default-features = false, features = ["async"] } +ethers-core = { git = "https://github.com/gakonst/ethers-rs", default-features = false } # cli clap = { version = "3.0.10", features = [ diff --git a/doc/src/writer/as_code.rs b/doc/src/writer/as_code.rs index faada228e5da..cb6ee914c154 100644 --- a/doc/src/writer/as_code.rs +++ b/doc/src/writer/as_code.rs @@ -1,3 +1,6 @@ +use std::str::FromStr; + +use ethers_core::{types::H160, utils::to_checksum}; use forge_fmt::solang_ext::AttrSortKeyIteratorExt; use itertools::Itertools; use solang_parser::pt::{ @@ -6,6 +9,7 @@ use solang_parser::pt::{ StructDefinition, Type, VariableAttribute, VariableDeclaration, VariableDefinition, }; +// TODO: delegate this logic to [forge_fmt::Formatter] /// Display Solidity parse tree item as code string. #[auto_impl::auto_impl(&)] pub trait AsCode { @@ -21,7 +25,12 @@ impl AsCode for VariableDefinition { attrs.insert(0, ' '); } let name = self.name.name.to_owned(); - format!("{ty}{attrs} {name}") + let init = self + .initializer + .as_ref() + .map(|init| format!(" = {}", init.as_code())) + .unwrap_or_default(); + format!("{ty}{attrs} {name}{init}") } } @@ -97,6 +106,28 @@ impl AsCode for Expression { Expression::MemberAccess(_, expr, ident) => { format!("{}.{}", ident.name, expr.as_code()) } + Expression::Parenthesis(_, expr) => { + format!("({})", expr.as_code()) + } + Expression::HexNumberLiteral(_, val) => { + // ref: https://docs.soliditylang.org/en/latest/types.html?highlight=address%20literal#address-literals + if val.len() == 42 { + to_checksum(&H160::from_str(val).expect(""), None) + } else { + val.to_owned() + } + } + Expression::NumberLiteral(_, val, exp) => { + let mut val = val.replace('_', ""); + if !exp.is_empty() { + val.push_str(&format!("e{}", exp.replace('_', ""))); + } + val + } + Expression::FunctionCall(_, expr, exprs) => { + format!("{}({})", expr.as_code(), exprs.iter().map(AsCode::as_code).join(", ")) + } + // TODO: assignments item => { panic!("Attempted to format unsupported item: {item:?}") } diff --git a/doc/src/writer/writer.rs b/doc/src/writer/writer.rs index af60ac79a03b..5eb0c25ac8df 100644 --- a/doc/src/writer/writer.rs +++ b/doc/src/writer/writer.rs @@ -116,7 +116,7 @@ impl BufWriter { _ => return Err(fmt::Error), }; - self.write_heading(heading)?; + self.write_bold(heading)?; self.writeln()?; self.write_piped(&Self::PARAM_TABLE_HEADERS.join("|"))?; From 4d04bb32ac0e1bbc25142b0bbf393a6a4e13714f Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Sun, 8 Jan 2023 19:03:08 +0200 Subject: [PATCH 30/67] handle files with constants --- doc/src/builder.rs | 65 +++- doc/src/document.rs | 34 ++- doc/src/parser/item.rs | 9 +- doc/src/preprocessor/contract_inheritance.rs | 39 ++- doc/src/preprocessor/mod.rs | 10 +- doc/src/writer/as_doc.rs | 301 ++++++++++--------- 6 files changed, 273 insertions(+), 185 deletions(-) diff --git a/doc/src/builder.rs b/doc/src/builder.rs index 55bb43426770..86d8188478bd 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -9,7 +9,7 @@ use std::{ path::{Path, PathBuf}, }; -use crate::{AsDoc, BufWriter, Document, Parser, Preprocessor}; +use crate::{AsDoc, BufWriter, Document, ParseItem, ParseSource, Parser, Preprocessor}; /// Build Solidity documentation for a project from natspec comments. /// The builder parses the source files using [Parser], @@ -24,7 +24,7 @@ pub struct DocBuilder { pub out: PathBuf, /// The documentation title. pub title: String, - /// THe + /// The array of preprocessors to apply. pub preprocessors: Vec>, } @@ -88,30 +88,67 @@ impl DocBuilder { })?; let mut doc = Parser::new(comments); source_unit.visit(&mut doc)?; - doc.items() + + // Split the parsed items on top-level constants and rest. + let (items, consts): (Vec, Vec) = doc + .items() + .into_iter() + .partition(|item| !matches!(item.source, ParseSource::Variable(_))); + + // Each regular item will be written into its own file. + let mut files = items .into_iter() .map(|item| { let relative_path = path.strip_prefix(&self.root)?.join(item.filename()); let target_path = self.out.join(Self::SRC).join(relative_path); - Ok(Document::new(item, path.clone(), target_path)) + Ok(Document::new(path.clone(), target_path).with_item(item)) }) - .collect::>>() + .collect::>>()?; + + // If top-level constants exist, they will be written to the same file. + if !consts.is_empty() { + let filestem = path.file_stem().and_then(|stem| stem.to_str()); + + let filename = { + let mut name = "constants".to_owned(); + if let Some(stem) = filestem { + name.push_str(&format!(".{stem}")); + } + name.push_str(".md"); + name + }; + let relative_path = path.strip_prefix(&self.root)?.join(filename); + let target_path = self.out.join(Self::SRC).join(relative_path); + + let identity = match filestem { + Some(stem) if stem.to_lowercase().contains("constants") => stem.to_owned(), + Some(stem) => format!("{stem} Constants"), + None => "Constants".to_owned(), + }; + + files + .push(Document::new(path.clone(), target_path).with_items(identity, consts)) + } + + Ok(files) }) .collect::>>()?; - // Flatten and sort the results - let documents = documents.into_iter().flatten().sorted_by(|doc1, doc2| { - doc1.item_path.display().to_string().cmp(&doc2.item_path.display().to_string()) - }); - - // Apply preprocessors to files + // Flatten results and apply preprocessors to files let documents = self .preprocessors .iter() - .try_fold(documents.collect::>(), |docs, p| p.preprocess(docs))?; + .try_fold(documents.into_iter().flatten().collect_vec(), |docs, p| { + p.preprocess(docs) + })?; + + // Sort the results + let documents = documents.into_iter().sorted_by(|doc1, doc2| { + doc1.item_path.display().to_string().cmp(&doc2.item_path.display().to_string()) + }); // Write mdbook related files - self.write_mdbook(documents)?; + self.write_mdbook(documents.collect_vec())?; Ok(()) } @@ -219,7 +256,7 @@ impl DocBuilder { for (path, files) in grouped { if path.extension().map(|ext| ext.eq(Self::SOL_EXT)).unwrap_or_default() { for file in files { - let ident = file.item.source.ident(); + let ident = &file.identity; let summary_path = file.target_path.strip_prefix("docs/src")?; summary.write_link_list_item( diff --git a/doc/src/document.rs b/doc/src/document.rs index 9d3f31456ffa..59acfc82433e 100644 --- a/doc/src/document.rs +++ b/doc/src/document.rs @@ -1,27 +1,47 @@ -use derive_more::Deref; use std::{collections::HashMap, path::PathBuf, sync::Mutex}; use crate::{ParseItem, PreprocessorId, PreprocessorOutput}; /// The wrapper around the [ParseItem] containing additional /// information the original item and extra context for outputting it. -#[derive(Debug, Deref)] +#[derive(Debug)] pub struct Document { - /// The underlying parsed item. - #[deref] - pub item: ParseItem, + /// The underlying parsed items. + pub items: Vec, /// The original item path. pub item_path: PathBuf, /// The target path where the document will be written. pub target_path: PathBuf, + /// The document display identity. + pub identity: String, /// The preprocessors results. context: Mutex>, } impl Document { /// Create new instance of [Document]. - pub fn new(item: ParseItem, item_path: PathBuf, target_path: PathBuf) -> Self { - Self { item, item_path, target_path, context: Mutex::new(HashMap::default()) } + pub fn new(item_path: PathBuf, target_path: PathBuf) -> Self { + Self { + item_path, + target_path, + items: Vec::default(), + identity: String::default(), + context: Mutex::new(HashMap::default()), + } + } + + /// Set item and item's identity on the [Document]. + pub fn with_item(mut self, item: ParseItem) -> Self { + self.identity = item.source.ident(); + self.items = vec![item]; + self + } + + /// Set items and some identity on the [Document]. + pub fn with_items(mut self, identity: String, items: Vec) -> Self { + self.identity = identity; + self.items = items; + self } /// Add a preprocessor result to inner document context. diff --git a/doc/src/parser/item.rs b/doc/src/parser/item.rs index 6b0844e5c0f0..d2fd590acba9 100644 --- a/doc/src/parser/item.rs +++ b/doc/src/parser/item.rs @@ -6,7 +6,7 @@ use solang_parser::pt::{ use crate::Comments; /// The parsed item. -#[derive(Debug, PartialEq)] +#[derive(PartialEq, Debug)] pub struct ParseItem { /// The parse tree source. pub source: ParseSource, @@ -64,6 +64,12 @@ impl ParseItem { self } + /// Set children on the [ParseItem]. + pub fn with_children(mut self, children: Vec) -> Self { + self.children = children; + self + } + /// Format the item's filename. pub fn filename(&self) -> String { let prefix = match self.source { @@ -87,6 +93,7 @@ impl ParseItem { filter_children_fn!(pub fn enums(&self, Enum) -> EnumDefinition); as_inner_source!(pub fn as_contract(&self, Contract) -> ContractDefinition); + as_inner_source!(pub fn as_variable(&self, Variable) -> VariableDefinition); } /// A wrapper type around pt token. diff --git a/doc/src/preprocessor/contract_inheritance.rs b/doc/src/preprocessor/contract_inheritance.rs index cca978bae7f3..cdf4a27b0434 100644 --- a/doc/src/preprocessor/contract_inheritance.rs +++ b/doc/src/preprocessor/contract_inheritance.rs @@ -1,5 +1,5 @@ use super::{Preprocessor, PreprocessorId}; -use crate::{Document, PreprocessorOutput}; +use crate::{Document, ParseSource, PreprocessorOutput}; use std::{collections::HashMap, path::PathBuf}; /// [ContractInheritance] preprocessor id. @@ -9,6 +9,8 @@ pub const CONTRACT_INHERITANCE_ID: PreprocessorId = PreprocessorId("contract_inh /// It matches the documents with inner [`ParseSource::Contract`](crate::ParseSource) elements, /// iterates over their [Base](solang_parser::pt::Base)s and attempts /// to link them with the paths of the other contract documents. +/// +/// This preprocessor writes to [Document]'s context. #[derive(Debug)] pub struct ContractInheritance; @@ -19,20 +21,23 @@ impl Preprocessor for ContractInheritance { fn preprocess(&self, documents: Vec) -> Result, eyre::Error> { for document in documents.iter() { - if let Some(contract) = document.as_contract() { - let mut links = HashMap::default(); - - // Attempt to match bases to other contracts - for base in contract.base.iter() { - let base_ident = base.name.identifiers.last().unwrap().name.clone(); - if let Some(linked) = self.try_link_base(&base_ident, &documents) { - links.insert(base_ident, linked); + for item in document.items.iter() { + if let ParseSource::Contract(ref contract) = item.source { + let mut links = HashMap::default(); + + // Attempt to match bases to other contracts + for base in contract.base.iter() { + let base_ident = base.name.identifiers.last().unwrap().name.clone(); + if let Some(linked) = self.try_link_base(&base_ident, &documents) { + links.insert(base_ident, linked); + } } - } - if !links.is_empty() { - // Write to context - document.add_context(self.id(), PreprocessorOutput::ContractInheritance(links)); + if !links.is_empty() { + // Write to context + document + .add_context(self.id(), PreprocessorOutput::ContractInheritance(links)); + } } } } @@ -44,9 +49,11 @@ impl Preprocessor for ContractInheritance { impl ContractInheritance { fn try_link_base<'a>(&self, base: &str, documents: &Vec) -> Option { for candidate in documents { - if let Some(contract) = candidate.as_contract() { - if base == contract.name.name { - return Some(candidate.target_path.clone()) + for item in candidate.items.iter() { + if let ParseSource::Contract(ref contract) = item.source { + if base == contract.name.name { + return Some(candidate.target_path.clone()) + } } } } diff --git a/doc/src/preprocessor/mod.rs b/doc/src/preprocessor/mod.rs index 965dd5694765..4b364a7487e2 100644 --- a/doc/src/preprocessor/mod.rs +++ b/doc/src/preprocessor/mod.rs @@ -1,12 +1,11 @@ //! Module containing documentation preprocessors. -mod contract_inheritance; +use crate::Document; use std::{collections::HashMap, fmt::Debug, path::PathBuf}; +mod contract_inheritance; pub use contract_inheritance::{ContractInheritance, CONTRACT_INHERITANCE_ID}; -use crate::Document; - /// The preprocessor id. #[derive(Debug, Eq, Hash, PartialEq)] pub struct PreprocessorId(&'static str); @@ -24,11 +23,6 @@ pub enum PreprocessorOutput { /// Trait for preprocessing and/or modifying existing documents /// before writing the to disk. pub trait Preprocessor: Debug { - // TODO: - /// Preprocessor must specify the output type it's writing to the - /// document context. This is the inner type in [PreprocessorOutput] variants. - // type Output = (); - /// The id of the preprocessor. /// Used to write data to document context. fn id(&self) -> PreprocessorId; diff --git a/doc/src/writer/as_doc.rs b/doc/src/writer/as_doc.rs index 77d01bcdff73..43a54cdf7aff 100644 --- a/doc/src/writer/as_doc.rs +++ b/doc/src/writer/as_doc.rs @@ -63,161 +63,184 @@ impl AsDoc for Document { fn as_doc(&self) -> AsDocResult { let mut writer = BufWriter::default(); - match &self.item.source { - ParseSource::Contract(contract) => { - writer.write_title(&contract.name.name)?; - - if !contract.base.is_empty() { - writer.write_bold("Inherits:")?; - - let mut bases = vec![]; - let linked = read_context!(self, CONTRACT_INHERITANCE_ID, ContractInheritance); - for base in contract.base.iter() { - let base_doc = base.as_doc()?; - let base_ident = &base.name.identifiers.last().unwrap().name; - bases.push( - linked - .as_ref() - .and_then(|l| { - l.get(base_ident).map(|path| { - let path = Path::new("/").join( - // TODO: move to func - path.strip_prefix("docs/src").ok().unwrap_or(path), - ); - Markdown::Link(&base_doc, &path.display().to_string()) - .as_doc() - }) - }) - .transpose()? - .unwrap_or(base_doc), - ) - } - - writer.writeln_raw(bases.join(", "))?; - writer.writeln()?; - } + // Handle constant files + if self.items.iter().all(|item| matches!(item.source, ParseSource::Variable(_))) { + writer.write_title("Constants")?; + + for item in self.items.iter() { + let var = item.as_variable().unwrap(); + writer.write_heading(&var.name.name)?; + writer.write_section(var, &item.comments)?; + } - writer.writeln_doc(&self.comments)?; + return Ok(writer.finish()) + } - if let Some(state_vars) = self.variables() { - writer.write_subtitle("State Variables")?; - state_vars - .into_iter() - .try_for_each(|(item, comments)| writer.write_section(item, comments))?; - } + for item in self.items.iter() { + match &item.source { + ParseSource::Contract(contract) => { + writer.write_title(&contract.name.name)?; + + if !contract.base.is_empty() { + writer.write_bold("Inherits:")?; + + let mut bases = vec![]; + let linked = + read_context!(self, CONTRACT_INHERITANCE_ID, ContractInheritance); + for base in contract.base.iter() { + let base_doc = base.as_doc()?; + let base_ident = &base.name.identifiers.last().unwrap().name; + bases.push( + linked + .as_ref() + .and_then(|l| { + l.get(base_ident).map(|path| { + let path = Path::new("/").join( + // TODO: move to func + path.strip_prefix("docs/src").ok().unwrap_or(path), + ); + Markdown::Link(&base_doc, &path.display().to_string()) + .as_doc() + }) + }) + .transpose()? + .unwrap_or(base_doc), + ) + } - if let Some(funcs) = self.functions() { - writer.write_subtitle("Functions")?; - funcs.into_iter().try_for_each(|(func, comments)| { - // Write function name - let func_name = - func.name.as_ref().map_or(func.ty.to_string(), |n| n.name.to_owned()); - writer.write_heading(&func_name)?; + writer.writeln_raw(bases.join(", "))?; writer.writeln()?; + } - // Write function docs - writer.writeln_doc( - comments.exclude_tags(&[CommentTag::Param, CommentTag::Return]), - )?; + writer.writeln_doc(&item.comments)?; - // Write function header - writer.write_code(func)?; + if let Some(state_vars) = item.variables() { + writer.write_subtitle("State Variables")?; + state_vars.into_iter().try_for_each(|(item, comments)| { + writer.write_section(item, comments) + })?; + } - // Write function parameter comments in a table - let params = - func.params.iter().filter_map(|p| p.1.as_ref()).collect::>(); - writer.try_write_param_table(CommentTag::Param, ¶ms, comments)?; + if let Some(funcs) = item.functions() { + writer.write_subtitle("Functions")?; + funcs.into_iter().try_for_each(|(func, comments)| { + // Write function name + let func_name = func + .name + .as_ref() + .map_or(func.ty.to_string(), |n| n.name.to_owned()); + writer.write_heading(&func_name)?; + writer.writeln()?; + + // Write function docs + writer.writeln_doc( + comments.exclude_tags(&[CommentTag::Param, CommentTag::Return]), + )?; + + // Write function header + writer.write_code(func)?; + + // Write function parameter comments in a table + let params = + func.params.iter().filter_map(|p| p.1.as_ref()).collect::>(); + writer.try_write_param_table(CommentTag::Param, ¶ms, comments)?; + + // Write function parameter comments in a table + let returns = func + .returns + .iter() + .filter_map(|p| p.1.as_ref()) + .collect::>(); + writer.try_write_param_table(CommentTag::Return, &returns, comments)?; + + writer.writeln()?; + + Ok::<(), std::fmt::Error>(()) + })?; + } - // Write function parameter comments in a table - let returns = - func.returns.iter().filter_map(|p| p.1.as_ref()).collect::>(); - writer.try_write_param_table(CommentTag::Return, &returns, comments)?; + if let Some(events) = item.events() { + writer.write_subtitle("Events")?; + events.into_iter().try_for_each(|(item, comments)| { + writer.write_heading(&item.name.name)?; + writer.write_section(item, comments) + })?; + } - writer.writeln()?; + if let Some(errors) = item.errors() { + writer.write_subtitle("Errors")?; + errors.into_iter().try_for_each(|(item, comments)| { + writer.write_heading(&item.name.name)?; + writer.write_section(item, comments) + })?; + } - Ok::<(), std::fmt::Error>(()) - })?; - } + if let Some(structs) = item.structs() { + writer.write_subtitle("Structs")?; + structs.into_iter().try_for_each(|(item, comments)| { + writer.write_heading(&item.name.name)?; + writer.write_section(item, comments) + })?; + } - if let Some(events) = self.events() { - writer.write_subtitle("Events")?; - events.into_iter().try_for_each(|(item, comments)| { - writer.write_heading(&item.name.name)?; - writer.write_section(item, comments) - })?; + if let Some(enums) = item.enums() { + writer.write_subtitle("Enums")?; + enums.into_iter().try_for_each(|(item, comments)| { + writer.write_heading(&item.name.name)?; + writer.write_section(item, comments) + })?; + } } - - if let Some(errors) = self.errors() { - writer.write_subtitle("Errors")?; - errors.into_iter().try_for_each(|(item, comments)| { - writer.write_heading(&item.name.name)?; - writer.write_section(item, comments) - })?; + ParseSource::Variable(var) => { + writer.write_title(&var.name.name)?; + writer.write_section(var, &item.comments)?; } - - if let Some(structs) = self.structs() { - writer.write_subtitle("Structs")?; - structs.into_iter().try_for_each(|(item, comments)| { - writer.write_heading(&item.name.name)?; - writer.write_section(item, comments) - })?; + ParseSource::Event(event) => { + writer.write_title(&event.name.name)?; + writer.write_section(event, &item.comments)?; + } + ParseSource::Error(error) => { + writer.write_title(&error.name.name)?; + writer.write_section(error, &item.comments)?; + } + ParseSource::Struct(structure) => { + writer.write_title(&structure.name.name)?; + writer.write_section(structure, &item.comments)?; + } + ParseSource::Enum(enumerable) => { + writer.write_title(&enumerable.name.name)?; + writer.write_section(enumerable, &item.comments)?; } + ParseSource::Function(func) => { + // TODO: cleanup + // Write function name + let func_name = + func.name.as_ref().map_or(func.ty.to_string(), |n| n.name.to_owned()); + writer.write_heading(&func_name)?; + writer.writeln()?; + + // Write function docs + writer.writeln_doc( + item.comments.exclude_tags(&[CommentTag::Param, CommentTag::Return]), + )?; + + // Write function header + writer.write_code(func)?; + + // Write function parameter comments in a table + let params = + func.params.iter().filter_map(|p| p.1.as_ref()).collect::>(); + writer.try_write_param_table(CommentTag::Param, ¶ms, &item.comments)?; + + // Write function parameter comments in a table + let returns = + func.returns.iter().filter_map(|p| p.1.as_ref()).collect::>(); + writer.try_write_param_table(CommentTag::Return, &returns, &item.comments)?; - if let Some(enums) = self.enums() { - writer.write_subtitle("Enums")?; - enums.into_iter().try_for_each(|(item, comments)| { - writer.write_heading(&item.name.name)?; - writer.write_section(item, comments) - })?; + writer.writeln()?; } - } - ParseSource::Variable(var) => { - writer.write_title(&var.name.name)?; - writer.write_section(var, &self.comments)?; - } - ParseSource::Event(event) => { - writer.write_title(&event.name.name)?; - writer.write_section(event, &self.comments)?; - } - ParseSource::Error(error) => { - writer.write_title(&error.name.name)?; - writer.write_section(error, &self.comments)?; - } - ParseSource::Struct(structure) => { - writer.write_title(&structure.name.name)?; - writer.write_section(structure, &self.comments)?; - } - ParseSource::Enum(enumerable) => { - writer.write_title(&enumerable.name.name)?; - writer.write_section(enumerable, &self.comments)?; - } - ParseSource::Function(func) => { - // TODO: cleanup - // Write function name - let func_name = - func.name.as_ref().map_or(func.ty.to_string(), |n| n.name.to_owned()); - writer.write_heading(&func_name)?; - writer.writeln()?; - - // Write function docs - writer.writeln_doc( - self.comments.exclude_tags(&[CommentTag::Param, CommentTag::Return]), - )?; - - // Write function header - writer.write_code(func)?; - - // Write function parameter comments in a table - let params = func.params.iter().filter_map(|p| p.1.as_ref()).collect::>(); - writer.try_write_param_table(CommentTag::Param, ¶ms, &self.comments)?; - - // Write function parameter comments in a table - let returns = func.returns.iter().filter_map(|p| p.1.as_ref()).collect::>(); - writer.try_write_param_table(CommentTag::Return, &returns, &self.comments)?; - - writer.writeln()?; - } - }; + }; + } Ok(writer.finish()) } From 03f5401da624ff87ab29ac51939e62578eb19af2 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Mon, 9 Jan 2023 10:24:26 +0200 Subject: [PATCH 31/67] add missing expressions in as_code impl --- doc/src/writer/as_code.rs | 86 +++++++++++++++++++++++++++++++++- doc/src/writer/as_doc.rs | 4 +- fmt/Cargo.toml | 13 +++-- fmt/src/formatter.rs | 4 +- fmt/src/solang_ext/operator.rs | 83 ++++++++++++++++++++++++++++++-- 5 files changed, 179 insertions(+), 11 deletions(-) diff --git a/doc/src/writer/as_code.rs b/doc/src/writer/as_code.rs index cb6ee914c154..4503f99ef03e 100644 --- a/doc/src/writer/as_code.rs +++ b/doc/src/writer/as_code.rs @@ -1,7 +1,7 @@ use std::str::FromStr; use ethers_core::{types::H160, utils::to_checksum}; -use forge_fmt::solang_ext::AttrSortKeyIteratorExt; +use forge_fmt::solang_ext::{AsStr, AttrSortKeyIteratorExt, Operator, OperatorComponents}; use itertools::Itertools; use solang_parser::pt::{ EnumDefinition, ErrorDefinition, ErrorParameter, EventDefinition, EventParameter, Expression, @@ -124,10 +124,92 @@ impl AsCode for Expression { } val } + Expression::StringLiteral(vals) => vals + .iter() + .map(|val| { + format!("{}\"{}\"", if val.unicode { "unicode" } else { "" }, val.string) + }) + .join(" "), + Expression::BoolLiteral(_, bool) => { + let val = if *bool { "true" } else { "false" }; + val.to_owned() + } + Expression::HexLiteral(vals) => { + vals.iter().map(|val| format!("hex\"{}\"", val.hex)).join(" ") + } + Expression::ArrayLiteral(_, exprs) => { + format!("[{}]", exprs.iter().map(AsCode::as_code).join(", ")) + } + Expression::RationalNumberLiteral(_, val, fraction, exp) => { + let mut val = val.replace('_', ""); + if val.is_empty() { + val = "0".to_owned(); + } + + let mut fraction = fraction.trim_end_matches('0').to_owned(); + if fraction.is_empty() { + fraction.push('0') + } + val.push_str(&format!(".{fraction}")); + + if !exp.is_empty() { + val.push_str(&format!("e{}", exp.replace('_', ""))); + } + val + } Expression::FunctionCall(_, expr, exprs) => { format!("{}({})", expr.as_code(), exprs.iter().map(AsCode::as_code).join(", ")) } - // TODO: assignments + Expression::Unit(_, expr, unit) => { + format!("{} {}", expr.as_code(), unit.as_str()) + } + Expression::PreIncrement(..) | + Expression::PostIncrement(..) | + Expression::PreDecrement(..) | + Expression::PostDecrement(..) | + Expression::Not(..) | + Expression::Complement(..) | + Expression::UnaryPlus(..) | + Expression::Add(..) | + Expression::UnaryMinus(..) | + Expression::Subtract(..) | + Expression::Power(..) | + Expression::Multiply(..) | + Expression::Divide(..) | + Expression::Modulo(..) | + Expression::ShiftLeft(..) | + Expression::ShiftRight(..) | + Expression::BitwiseAnd(..) | + Expression::BitwiseXor(..) | + Expression::BitwiseOr(..) | + Expression::Less(..) | + Expression::More(..) | + Expression::LessEqual(..) | + Expression::MoreEqual(..) | + Expression::And(..) | + Expression::Or(..) | + Expression::Equal(..) | + Expression::NotEqual(..) => { + let spaced = self.has_space_around(); + + let (left, right) = self.components(); + + let mut val = String::from(self.operator().unwrap()); + if let Some(left) = left { + if spaced { + val.insert(0, ' '); + } + val.insert_str(0, &left.as_code()); + } + if let Some(right) = right { + if spaced { + val.push(' '); + } + val.push_str(&right.as_code()) + } + + val + } item => { panic!("Attempted to format unsupported item: {item:?}") } diff --git a/doc/src/writer/as_doc.rs b/doc/src/writer/as_doc.rs index 43a54cdf7aff..ec1e58ae427d 100644 --- a/doc/src/writer/as_doc.rs +++ b/doc/src/writer/as_doc.rs @@ -117,7 +117,9 @@ impl AsDoc for Document { if let Some(state_vars) = item.variables() { writer.write_subtitle("State Variables")?; state_vars.into_iter().try_for_each(|(item, comments)| { - writer.write_section(item, comments) + writer.write_heading(&item.name.name)?; + writer.write_section(item, comments)?; + writer.writeln() })?; } diff --git a/fmt/Cargo.toml b/fmt/Cargo.toml index c4c2fb91bb0d..fb019af22d64 100644 --- a/fmt/Cargo.toml +++ b/fmt/Cargo.toml @@ -9,12 +9,19 @@ repository = "https://github.com/foundry-rs/foundry" keywords = ["ethereum", "web3", "solidity", "linter"] [dependencies] -semver = "1.0.4" +# foundry dep +foundry-config = { path = "../config" } + +# ethers +ethers-core = { git = "https://github.com/gakonst/ethers-rs", default-features = false } + +# parser solang-parser = "=0.1.18" + +# misc +semver = "1.0.4" itertools = "0.10.3" thiserror = "1.0.30" -ethers-core = { git = "https://github.com/gakonst/ethers-rs", default-features = false } -foundry-config = { path = "../config" } [dev-dependencies] pretty_assertions = "1.0.0" diff --git a/fmt/src/formatter.rs b/fmt/src/formatter.rs index fef94d99bf32..0fe9729e7df4 100644 --- a/fmt/src/formatter.rs +++ b/fmt/src/formatter.rs @@ -2064,7 +2064,7 @@ impl<'a, W: Write> Visitor for Formatter<'a, W> { let spaced = expr.has_space_around(); let op = expr.operator().unwrap(); - match expr.into_components() { + match expr.components_mut() { (Some(left), Some(right)) => { left.visit(self)?; @@ -2103,7 +2103,7 @@ impl<'a, W: Write> Visitor for Formatter<'a, W> { Expression::AssignDivide(..) | Expression::AssignModulo(..) => { let op = expr.operator().unwrap(); - let (left, right) = expr.into_components(); + let (left, right) = expr.components_mut(); let (left, right) = (left.unwrap(), right.unwrap()); left.visit(self)?; diff --git a/fmt/src/solang_ext/operator.rs b/fmt/src/solang_ext/operator.rs index 1148ab4eb8a3..67eaf832ff93 100644 --- a/fmt/src/solang_ext/operator.rs +++ b/fmt/src/solang_ext/operator.rs @@ -5,10 +5,15 @@ pub trait Operator: Sized { fn unsplittable(&self) -> bool; fn operator(&self) -> Option<&'static str>; fn has_space_around(&self) -> bool; - fn into_components(self) -> (Option, Option); } -impl Operator for &mut Expression { +/// Splits the iterator into components +pub trait OperatorComponents: Sized { + fn components(&self) -> (Option<&Self>, Option<&Self>); + fn components_mut(&mut self) -> (Option<&mut Self>, Option<&mut Self>); +} + +impl Operator for Expression { fn unsplittable(&self) -> bool { use Expression::*; matches!( @@ -25,6 +30,7 @@ impl Operator for &mut Expression { This(..) ) } + fn operator(&self) -> Option<&'static str> { use Expression::*; Some(match self { @@ -87,6 +93,7 @@ impl Operator for &mut Expression { Parenthesis(..) => return None, }) } + fn has_space_around(&self) -> bool { use Expression::*; !matches!( @@ -101,8 +108,78 @@ impl Operator for &mut Expression { UnaryMinus(..) ) } - fn into_components(self) -> (Option, Option) { +} + +impl OperatorComponents for Expression { + fn components(&self) -> (Option<&Self>, Option<&Self>) { use Expression::*; + match self { + PostDecrement(_, expr) | PostIncrement(_, expr) => (Some(expr), None), + Not(_, expr) | + Complement(_, expr) | + New(_, expr) | + Delete(_, expr) | + UnaryPlus(_, expr) | + UnaryMinus(_, expr) | + PreDecrement(_, expr) | + Parenthesis(_, expr) | + PreIncrement(_, expr) => (None, Some(expr)), + Power(_, left, right) | + Multiply(_, left, right) | + Divide(_, left, right) | + Modulo(_, left, right) | + Add(_, left, right) | + Subtract(_, left, right) | + ShiftLeft(_, left, right) | + ShiftRight(_, left, right) | + BitwiseAnd(_, left, right) | + BitwiseXor(_, left, right) | + BitwiseOr(_, left, right) | + Less(_, left, right) | + More(_, left, right) | + LessEqual(_, left, right) | + MoreEqual(_, left, right) | + Equal(_, left, right) | + NotEqual(_, left, right) | + And(_, left, right) | + Or(_, left, right) | + Assign(_, left, right) | + AssignOr(_, left, right) | + AssignAnd(_, left, right) | + AssignXor(_, left, right) | + AssignShiftLeft(_, left, right) | + AssignShiftRight(_, left, right) | + AssignAdd(_, left, right) | + AssignSubtract(_, left, right) | + AssignMultiply(_, left, right) | + AssignDivide(_, left, right) | + AssignModulo(_, left, right) => (Some(left), Some(right)), + MemberAccess(..) | + Ternary(..) | + ArraySubscript(..) | + ArraySlice(..) | + FunctionCall(..) | + FunctionCallBlock(..) | + NamedFunctionCall(..) | + BoolLiteral(..) | + NumberLiteral(..) | + RationalNumberLiteral(..) | + HexNumberLiteral(..) | + StringLiteral(..) | + Type(..) | + HexLiteral(..) | + AddressLiteral(..) | + Variable(..) | + List(..) | + ArrayLiteral(..) | + Unit(..) | + This(..) => (None, None), + } + } + + fn components_mut(&mut self) -> (Option<&mut Self>, Option<&mut Self>) { + use Expression::*; + match self { PostDecrement(_, expr) | PostIncrement(_, expr) => (Some(expr.as_mut()), None), Not(_, expr) | From 240bc7f03bc2b89aa29913fe083114294642d7c4 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Mon, 9 Jan 2023 11:12:11 +0200 Subject: [PATCH 32/67] exit early on no sources --- doc/src/builder.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/src/builder.rs b/doc/src/builder.rs index 86d8188478bd..01ceda053409 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -73,6 +73,12 @@ impl DocBuilder { pub fn build(self) -> eyre::Result<()> { // Collect and parse source files let sources: Vec<_> = source_files_iter(&self.sources).collect(); + + if sources.is_empty() { + println!("No sources detected at {}", self.sources.display()); + return Ok(()) + } + let documents = sources .par_iter() .enumerate() From 43479f68f282e696b7959ff07eb4148220cac8b0 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Mon, 9 Jan 2023 12:27:03 +0200 Subject: [PATCH 33/67] skip parentheses on shallow overrides --- doc/src/writer/as_code.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/doc/src/writer/as_code.rs b/doc/src/writer/as_code.rs index 4503f99ef03e..9d8ae78e13b9 100644 --- a/doc/src/writer/as_code.rs +++ b/doc/src/writer/as_code.rs @@ -41,7 +41,11 @@ impl AsCode for VariableAttribute { VariableAttribute::Constant(_) => "constant".to_owned(), VariableAttribute::Immutable(_) => "immutable".to_owned(), VariableAttribute::Override(_, idents) => { - format!("override({})", idents.iter().map(AsCode::as_code).join(", ")) + let mut val = "override".to_owned(); + if !idents.is_empty() { + val.push_str(&format!("({})", idents.iter().map(AsCode::as_code).join(", "))); + } + val } } } @@ -225,7 +229,11 @@ impl AsCode for FunctionAttribute { Self::Virtual(_) => "virtual".to_owned(), Self::Immutable(_) => "immutable".to_owned(), Self::Override(_, idents) => { - format!("override({})", idents.iter().map(AsCode::as_code).join(", ")) + let mut val = "override".to_owned(); + if !idents.is_empty() { + val.push_str(&format!("({})", idents.iter().map(AsCode::as_code).join(", "))); + } + val } Self::BaseOrModifier(_, _base) => "".to_owned(), // TODO: Self::NameValue(..) => unreachable!(), From 6e4e6b91aebe4ae35d9895ea2a4b99c84038ce14 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Mon, 9 Jan 2023 12:39:13 +0200 Subject: [PATCH 34/67] add type parsing & writing --- doc/src/parser/item.rs | 6 +++++- doc/src/parser/mod.rs | 8 +++++++- doc/src/writer/as_code.rs | 9 ++++++++- doc/src/writer/as_doc.rs | 4 ++++ 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/doc/src/parser/item.rs b/doc/src/parser/item.rs index d2fd590acba9..21f7da94d51a 100644 --- a/doc/src/parser/item.rs +++ b/doc/src/parser/item.rs @@ -1,6 +1,6 @@ use solang_parser::pt::{ ContractDefinition, EnumDefinition, ErrorDefinition, EventDefinition, FunctionDefinition, - StructDefinition, VariableDefinition, + StructDefinition, TypeDefinition, VariableDefinition, }; use crate::Comments; @@ -80,6 +80,7 @@ impl ParseItem { ParseSource::Error(_) => "error", ParseSource::Struct(_) => "struct", ParseSource::Enum(_) => "enum", + ParseSource::Type(_) => "type", }; let ident = self.source.ident(); format!("{prefix}.{ident}.md") @@ -113,6 +114,8 @@ pub enum ParseSource { Struct(StructDefinition), /// Source enum definition. Enum(EnumDefinition), + /// Source type definition. + Type(TypeDefinition), } impl ParseSource { @@ -128,6 +131,7 @@ impl ParseSource { ParseSource::Function(func) => { func.name.as_ref().map_or(func.ty.to_string(), |n| n.name.to_owned()) } + ParseSource::Type(ty) => ty.name.name.to_owned(), } } } diff --git a/doc/src/parser/mod.rs b/doc/src/parser/mod.rs index 13acd8762605..3ed1e2320252 100644 --- a/doc/src/parser/mod.rs +++ b/doc/src/parser/mod.rs @@ -5,7 +5,8 @@ use solang_parser::{ doccomment::{parse_doccomments, DocComment}, pt::{ Comment as SolangComment, EnumDefinition, ErrorDefinition, EventDefinition, - FunctionDefinition, Loc, SourceUnit, SourceUnitPart, StructDefinition, VariableDefinition, + FunctionDefinition, Loc, SourceUnit, SourceUnitPart, StructDefinition, TypeDefinition, + VariableDefinition, }, }; @@ -133,6 +134,7 @@ impl Visitor for Parser { SourceUnitPart::StructDefinition(structure) => self.visit_struct(structure)?, SourceUnitPart::EnumDefinition(enumerable) => self.visit_enum(enumerable)?, SourceUnitPart::VariableDefinition(var) => self.visit_var_definition(var)?, + SourceUnitPart::TypeDefinition(ty) => self.visit_type_definition(ty)?, _ => {} }; } @@ -163,6 +165,10 @@ impl Visitor for Parser { fn visit_enum(&mut self, enumerable: &mut EnumDefinition) -> ParserResult<()> { self.add_element_to_parent(ParseSource::Enum(enumerable.clone()), enumerable.loc) } + + fn visit_type_definition(&mut self, def: &mut TypeDefinition) -> ParserResult<()> { + self.add_element_to_parent(ParseSource::Type(def.clone()), def.loc) + } } #[cfg(test)] diff --git a/doc/src/writer/as_code.rs b/doc/src/writer/as_code.rs index 9d8ae78e13b9..c12d500ba333 100644 --- a/doc/src/writer/as_code.rs +++ b/doc/src/writer/as_code.rs @@ -6,7 +6,8 @@ use itertools::Itertools; use solang_parser::pt::{ EnumDefinition, ErrorDefinition, ErrorParameter, EventDefinition, EventParameter, Expression, FunctionAttribute, FunctionDefinition, Identifier, IdentifierPath, Loc, Parameter, - StructDefinition, Type, VariableAttribute, VariableDeclaration, VariableDefinition, + StructDefinition, Type, TypeDefinition, VariableAttribute, VariableDeclaration, + VariableDefinition, }; // TODO: delegate this logic to [forge_fmt::Formatter] @@ -342,6 +343,12 @@ impl AsCode for EnumDefinition { } } +impl AsCode for TypeDefinition { + fn as_code(&self) -> String { + format!("type {} is {}", &self.name.name, self.ty.as_code()) + } +} + impl AsCode for Identifier { fn as_code(&self) -> String { self.name.to_string() diff --git a/doc/src/writer/as_doc.rs b/doc/src/writer/as_doc.rs index ec1e58ae427d..e37d34048d35 100644 --- a/doc/src/writer/as_doc.rs +++ b/doc/src/writer/as_doc.rs @@ -213,6 +213,10 @@ impl AsDoc for Document { writer.write_title(&enumerable.name.name)?; writer.write_section(enumerable, &item.comments)?; } + ParseSource::Type(ty) => { + writer.write_title(&ty.name.name)?; + writer.write_section(ty, &item.comments)?; + } ParseSource::Function(func) => { // TODO: cleanup // Write function name From ad701e262e2c42c8e38b93399f4773caf576b7ec Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Mon, 9 Jan 2023 13:41:39 +0200 Subject: [PATCH 35/67] basic server --- Cargo.lock | 356 ++++++++++++++++++++++++++++++++++++++- cli/src/cmd/forge/doc.rs | 16 +- cli/src/opts/forge.rs | 2 +- doc/Cargo.toml | 6 + doc/src/lib.rs | 4 + doc/src/server.rs | 119 +++++++++++++ 6 files changed, 496 insertions(+), 7 deletions(-) create mode 100644 doc/src/server.rs diff --git a/Cargo.lock b/Cargo.lock index ea93bbce032c..16e62a19002b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,6 +81,19 @@ dependencies = [ "memchr", ] +[[package]] +name = "ammonia" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e6d1c7838db705c9b756557ee27c384ce695a1c51a6fe528784cb1c6840170" +dependencies = [ + "html5ever", + "maplit", + "once_cell", + "tendril", + "url", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -1684,6 +1697,18 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +[[package]] +name = "elasticlunr-rs" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94d9c8df0fe6879ca12e7633fdfe467c503722cc981fc463703472d2b876448" +dependencies = [ + "regex", + "serde", + "serde_derive", + "serde_json", +] + [[package]] name = "elliptic-curve" version = "0.12.3" @@ -1773,6 +1798,19 @@ dependencies = [ "syn", ] +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + [[package]] name = "errno" version = "0.2.8" @@ -2348,11 +2386,15 @@ dependencies = [ "eyre", "forge-fmt", "foundry-common", + "futures-util", "itertools", + "mdbook", "rayon", "solang-parser", "thiserror", + "tokio", "toml", + "warp", ] [[package]] @@ -2649,6 +2691,16 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + [[package]] name = "futures" version = "0.3.25" @@ -3011,6 +3063,15 @@ dependencies = [ "url", ] +[[package]] +name = "gitignore" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78aa90e4620c1498ac434c06ba6e521b525794bbdacf085d490cc794b4a2f9a4" +dependencies = [ + "glob", +] + [[package]] name = "glob" version = "0.3.0" @@ -3066,6 +3127,20 @@ version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" +[[package]] +name = "handlebars" +version = "4.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "035ef95d03713f2c347a72547b7cd38cbc9af7cd51e6099fb62d586d4a6dee3a" +dependencies = [ + "log", + "pest", + "pest_derive", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "hash-db" version = "0.15.2" @@ -3109,6 +3184,31 @@ dependencies = [ "fxhash", ] +[[package]] +name = "headers" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" +dependencies = [ + "base64 0.13.1", + "bitflags", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.4.0" @@ -3182,6 +3282,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "html5ever" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "http" version = "0.2.8" @@ -3222,6 +3336,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "0.14.23" @@ -3717,12 +3837,32 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + [[package]] name = "maplit" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" +[[package]] +name = "markup5ever" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" +dependencies = [ + "log", + "phf 0.10.1", + "phf_codegen 0.10.0", + "string_cache", + "string_cache_codegen", + "tendril", +] + [[package]] name = "matchers" version = "0.1.0" @@ -3764,6 +3904,40 @@ dependencies = [ "digest 0.10.6", ] +[[package]] +name = "mdbook" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1ed28d5903dde77bd5182645078a37ee57014cac6ccb2d54e1d6496386648e4" +dependencies = [ + "ammonia", + "anyhow", + "chrono", + "clap 4.0.32", + "clap_complete", + "elasticlunr-rs", + "env_logger", + "futures-util", + "gitignore", + "handlebars", + "log", + "memchr", + "notify", + "notify-debouncer-mini", + "once_cell", + "opener", + "pulldown-cmark", + "regex", + "serde", + "serde_json", + "shlex", + "tempfile", + "tokio", + "toml", + "topological-sort", + "warp", +] + [[package]] name = "memchr" version = "2.5.0" @@ -3837,6 +4011,16 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -4006,6 +4190,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "notify-debouncer-mini" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e23e9fa24f094b143c1eb61f90ac6457de87be6987bc70746e0179f7dbc9007b" +dependencies = [ + "crossbeam-channel", + "notify", +] + [[package]] name = "ntapi" version = "0.3.7" @@ -4205,6 +4399,16 @@ dependencies = [ "syn", ] +[[package]] +name = "opener" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea3ebcd72a54701f56345f16785a6d3ac2df7e986d273eb4395c0b01db17952" +dependencies = [ + "bstr 0.2.17", + "winapi", +] + [[package]] name = "openssl" version = "0.10.45" @@ -4457,6 +4661,50 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +[[package]] +name = "pest" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f6e86fb9e7026527a0d46bc308b841d73170ef8f443e1807f6ef88526a816d4" +dependencies = [ + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96504449aa860c8dcde14f9fba5c58dc6658688ca1fe363589d6327b8662c603" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "798e0220d1111ae63d66cb66a5dcb3fc2d986d520b98e49e1852bfdb11d7c5e7" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "984298b75898e30a843e278a9f2452c31e349a073a0ce6fd950a12a74464e065" +dependencies = [ + "once_cell", + "pest", + "sha1", +] + [[package]] name = "petgraph" version = "0.6.2" @@ -4507,6 +4755,16 @@ dependencies = [ "phf_shared 0.8.0", ] +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", +] + [[package]] name = "phf_generator" version = "0.8.0" @@ -4785,6 +5043,17 @@ version = "2.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" +[[package]] +name = "pulldown-cmark" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d9cc634bc78768157b5cbfe988ffcd1dcba95cd2b2f03a88316c08c6d00ed63" +dependencies = [ + "bitflags", + "memchr", + "unicase", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -5036,7 +5305,7 @@ dependencies = [ "pin-project-lite", "rustls", "rustls-native-certs", - "rustls-pemfile", + "rustls-pemfile 1.0.1", "serde", "serde_json", "serde_urlencoded", @@ -5314,11 +5583,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50" dependencies = [ "openssl-probe", - "rustls-pemfile", + "rustls-pemfile 1.0.1", "schannel", "security-framework", ] +[[package]] +name = "rustls-pemfile" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9" +dependencies = [ + "base64 0.13.1", +] + [[package]] name = "rustls-pemfile" version = "1.0.1" @@ -5427,6 +5705,12 @@ dependencies = [ "windows-sys 0.36.1", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.1.0" @@ -5851,6 +6135,19 @@ dependencies = [ "parking_lot 0.12.1", "phf_shared 0.10.0", "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro2", + "quote", ] [[package]] @@ -5993,6 +6290,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + [[package]] name = "term" version = "0.7.0" @@ -6043,7 +6351,7 @@ dependencies = [ "fnv", "nom 5.1.2", "phf 0.8.0", - "phf_codegen", + "phf_codegen 0.8.0", ] [[package]] @@ -6287,6 +6595,12 @@ dependencies = [ "itertools", ] +[[package]] +name = "topological-sort" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" + [[package]] name = "tower" version = "0.4.13" @@ -6531,6 +6845,12 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +[[package]] +name = "ucd-trie" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" + [[package]] name = "ui" version = "0.2.0" @@ -6729,6 +7049,36 @@ dependencies = [ "try-lock", ] +[[package]] +name = "warp" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7b8be92646fc3d18b06147664ebc5f48d222686cb11a8755e561a735aacc6d" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "headers", + "http", + "hyper", + "log", + "mime", + "mime_guess", + "percent-encoding", + "pin-project", + "rustls-pemfile 0.2.1", + "scoped-tls", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-stream", + "tokio-tungstenite 0.17.2", + "tokio-util", + "tower-service", + "tracing", +] + [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" diff --git a/cli/src/cmd/forge/doc.rs b/cli/src/cmd/forge/doc.rs index 59eab8fc59b9..8c4f9c433b21 100644 --- a/cli/src/cmd/forge/doc.rs +++ b/cli/src/cmd/forge/doc.rs @@ -1,6 +1,6 @@ use crate::cmd::Cmd; use clap::{Parser, ValueHint}; -use forge_doc::{ContractInheritance, DocBuilder}; +use forge_doc::{ContractInheritance, DocBuilder, Server}; use foundry_config::{find_project_root_path, load_config_with_root}; use std::path::PathBuf; @@ -24,6 +24,9 @@ pub struct DocArgs { value_name = "PATH" )] out: Option, + + #[clap(help = "Serve the documentation.", long, short)] + serve: bool, } impl Cmd for DocArgs { @@ -32,11 +35,18 @@ impl Cmd for DocArgs { fn run(self) -> eyre::Result { let root = self.root.clone().unwrap_or(find_project_root_path()?); let config = load_config_with_root(self.root.clone()); + let out = self.out.clone().unwrap_or(config.doc.out.clone()); DocBuilder::new(root, config.project_paths().sources) - .with_out(self.out.clone().unwrap_or(config.doc.out.clone())) + .with_out(out.clone()) .with_title(config.doc.title.clone()) .with_preprocessor(ContractInheritance) - .build() + .build()?; + + if self.serve { + Server::new(out).serve()?; + } + + Ok(()) } } diff --git a/cli/src/opts/forge.rs b/cli/src/opts/forge.rs index ef787180992e..67c30d5f9b86 100644 --- a/cli/src/opts/forge.rs +++ b/cli/src/opts/forge.rs @@ -151,7 +151,7 @@ pub enum Subcommands { )] Geiger(geiger::GeigerArgs), - #[clap(about = "Generate documentation for the project,")] + #[clap(about = "Generate documentation for the project.")] Doc(DocArgs), } diff --git a/doc/Cargo.toml b/doc/Cargo.toml index 7f94fcbb8ed3..c4abd9d4d67c 100644 --- a/doc/Cargo.toml +++ b/doc/Cargo.toml @@ -25,6 +25,12 @@ clap = { version = "3.0.10", features = [ "wrap_help", ] } +# mdbook +mdbook = "0.4.25" +warp = { version = "0.3.2", default-features = false, features = ["websocket"] } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +futures-util = "0.3.4" + # misc solang-parser = "=0.1.18" eyre = "0.6" diff --git a/doc/src/lib.rs b/doc/src/lib.rs index 221d2793cdcf..82a27715707a 100644 --- a/doc/src/lib.rs +++ b/doc/src/lib.rs @@ -13,11 +13,15 @@ mod builder; mod document; mod parser; mod preprocessor; +mod server; mod writer; /// The documentation builder. pub use builder::DocBuilder; +/// The documentation server. +pub use server::Server; + /// The document output. pub use document::Document; diff --git a/doc/src/server.rs b/doc/src/server.rs new file mode 100644 index 000000000000..079a36f5b0a3 --- /dev/null +++ b/doc/src/server.rs @@ -0,0 +1,119 @@ +use futures_util::{SinkExt, StreamExt}; +use mdbook::{utils::fs::get_404_output_file, MDBook}; +use std::{ + net::{SocketAddr, ToSocketAddrs}, + path::PathBuf, +}; +use tokio::sync::broadcast; +use warp::{ws::Message, Filter}; + +/// The HTTP endpoint for the websocket used to trigger reloads when a file changes. +const LIVE_RELOAD_ENDPOINT: &str = "__livereload"; + +/// Basic mdbook server. Given a path, hostname and port, serves the mdbook. +#[derive(Debug)] +pub struct Server { + path: PathBuf, + hostname: String, + port: String, +} + +impl Default for Server { + fn default() -> Self { + Self { path: PathBuf::default(), hostname: "localhost".to_owned(), port: "3000".to_owned() } + } +} + +impl Server { + /// Create new instance of [Server]. + pub fn new(path: PathBuf) -> Self { + Self { path, ..Default::default() } + } + + /// Serve the mdbook. + pub fn serve(self) -> eyre::Result<()> { + let mut book = + MDBook::load(&self.path).map_err(|err| eyre::eyre!("failed to load book: {err:?}"))?; + + let address = format!("{}:{}", self.hostname, self.port); + + let update_config = |book: &mut MDBook| { + book.config + .set("output.html.live-reload-endpoint", LIVE_RELOAD_ENDPOINT) + .expect("live-reload-endpoint update failed"); + // Override site-url for local serving of the 404 file + book.config.set("output.html.site-url", "/").unwrap(); + }; + update_config(&mut book); + book.build().map_err(|err| eyre::eyre!("failed to build book: {err:?}"))?; + + let sockaddr: SocketAddr = address + .to_socket_addrs()? + .next() + .ok_or_else(|| eyre::eyre!("no address found for {}", address))?; + let build_dir = book.build_dir_for("html"); + let input_404 = book + .config + .get("output.html.input-404") + .and_then(toml::Value::as_str) + .map(ToString::to_string); + let file_404 = get_404_output_file(&input_404); + + // A channel used to broadcast to any websockets to reload when a file changes. + let (tx, _rx) = tokio::sync::broadcast::channel::(100); + + let reload_tx = tx.clone(); + let thread_handle = std::thread::spawn(move || { + serve(build_dir, sockaddr, reload_tx, &file_404); + }); + + println!("Serving on: http://{address}"); + + let _ = thread_handle.join(); + + Ok(()) + } +} + +// Adapted from https://github.com/rust-lang/mdBook/blob/41a6f0d43e1a2d9543877eacb4cd2a017f9fe8da/src/cmd/serve.rs#L124 +#[tokio::main] +async fn serve( + build_dir: PathBuf, + address: SocketAddr, + reload_tx: broadcast::Sender, + file_404: &str, +) { + // A warp Filter which captures `reload_tx` and provides an `rx` copy to + // receive reload messages. + let sender = warp::any().map(move || reload_tx.subscribe()); + + // A warp Filter to handle the livereload endpoint. This upgrades to a + // websocket, and then waits for any filesystem change notifications, and + // relays them over the websocket. + let livereload = warp::path(LIVE_RELOAD_ENDPOINT).and(warp::ws()).and(sender).map( + |ws: warp::ws::Ws, mut rx: broadcast::Receiver| { + ws.on_upgrade(move |ws| async move { + let (mut user_ws_tx, _user_ws_rx) = ws.split(); + // TODO: trace!("websocket got connection"); + if let Ok(m) = rx.recv().await { + // TODO: trace!("notify of reload"); + let _ = user_ws_tx.send(m).await; + } + }) + }, + ); + // A warp Filter that serves from the filesystem. + let book_route = warp::fs::dir(build_dir.clone()); + // The fallback route for 404 errors + let fallback_route = warp::fs::file(build_dir.join(file_404)) + .map(|reply| warp::reply::with_status(reply, warp::http::StatusCode::NOT_FOUND)); + let routes = livereload.or(book_route).or(fallback_route); + + std::panic::set_hook(Box::new(move |_panic_info| { + // exit if serve panics + // TODO: error!("Unable to serve: {}", panic_info); + std::process::exit(1); + })); + + warp::serve(routes).run(address).await; +} From e3c4511fda8e950ecfd5db11d644d1ece6bc5bf2 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Mon, 9 Jan 2023 15:30:29 +0200 Subject: [PATCH 36/67] support overloaded functions --- doc/src/builder.rs | 48 ++- doc/src/document.rs | 27 +- doc/src/parser/item.rs | 1 + doc/src/preprocessor/contract_inheritance.rs | 6 +- doc/src/writer/as_doc.rs | 376 +++++++++++-------- 5 files changed, 271 insertions(+), 187 deletions(-) diff --git a/doc/src/builder.rs b/doc/src/builder.rs index 01ceda053409..db7d9b3e5edb 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -9,7 +9,10 @@ use std::{ path::{Path, PathBuf}, }; -use crate::{AsDoc, BufWriter, Document, ParseItem, ParseSource, Parser, Preprocessor}; +use crate::{ + document::DocumentContent, AsDoc, BufWriter, Document, ParseItem, ParseSource, Parser, + Preprocessor, +}; /// Build Solidity documentation for a project from natspec comments. /// The builder parses the source files using [Parser], @@ -101,13 +104,33 @@ impl DocBuilder { .into_iter() .partition(|item| !matches!(item.source, ParseSource::Variable(_))); + // TODO: group overloaded functions + // Attempt to group overloaded top-level functions + let mut remaining = Vec::with_capacity(items.len()); + let mut funcs: HashMap> = HashMap::default(); + for item in items { + if matches!(item.source, ParseSource::Function(_)) { + funcs.entry(item.source.ident()).or_default().push(item); + } else { + // Put the item back + remaining.push(item); + } + } + let (items, overloaded): ( + HashMap>, + HashMap>, + ) = funcs.into_iter().partition(|(_, v)| v.len() == 1); + remaining.extend(items.into_iter().flat_map(|(_, v)| v)); + // Each regular item will be written into its own file. - let mut files = items + let mut files = remaining .into_iter() .map(|item| { let relative_path = path.strip_prefix(&self.root)?.join(item.filename()); let target_path = self.out.join(Self::SRC).join(relative_path); - Ok(Document::new(path.clone(), target_path).with_item(item)) + let ident = item.source.ident(); + Ok(Document::new(path.clone(), target_path) + .with_content(DocumentContent::Single(item), ident)) }) .collect::>>()?; @@ -132,8 +155,23 @@ impl DocBuilder { None => "Constants".to_owned(), }; - files - .push(Document::new(path.clone(), target_path).with_items(identity, consts)) + files.push( + Document::new(path.clone(), target_path) + .with_content(DocumentContent::Constants(consts), identity), + ) + } + + // If overloaded functions exist, they will be written to the same file + if !overloaded.is_empty() { + for (ident, funcs) in overloaded { + let filename = funcs.first().expect("no overloaded functions").filename(); + let relative_path = path.strip_prefix(&self.root)?.join(filename); + let target_path = self.out.join(Self::SRC).join(relative_path); + files.push( + Document::new(path.clone(), target_path) + .with_content(DocumentContent::OverloadedFunctions(funcs), ident), + ); + } } Ok(files) diff --git a/doc/src/document.rs b/doc/src/document.rs index 59acfc82433e..bbc54e74111e 100644 --- a/doc/src/document.rs +++ b/doc/src/document.rs @@ -7,7 +7,7 @@ use crate::{ParseItem, PreprocessorId, PreprocessorOutput}; #[derive(Debug)] pub struct Document { /// The underlying parsed items. - pub items: Vec, + pub content: DocumentContent, /// The original item path. pub item_path: PathBuf, /// The target path where the document will be written. @@ -18,29 +18,32 @@ pub struct Document { context: Mutex>, } +/// The content of the document. +#[derive(Debug)] +pub enum DocumentContent { + Empty, + Single(ParseItem), + Constants(Vec), + OverloadedFunctions(Vec), +} + impl Document { /// Create new instance of [Document]. pub fn new(item_path: PathBuf, target_path: PathBuf) -> Self { Self { item_path, target_path, - items: Vec::default(), identity: String::default(), + content: DocumentContent::Empty, context: Mutex::new(HashMap::default()), } } - /// Set item and item's identity on the [Document]. - pub fn with_item(mut self, item: ParseItem) -> Self { - self.identity = item.source.ident(); - self.items = vec![item]; - self - } - - /// Set items and some identity on the [Document]. - pub fn with_items(mut self, identity: String, items: Vec) -> Self { + /// Set content and identity on the [Document]. + #[must_use] + pub fn with_content(mut self, content: DocumentContent, identity: String) -> Self { + self.content = content; self.identity = identity; - self.items = items; self } diff --git a/doc/src/parser/item.rs b/doc/src/parser/item.rs index 21f7da94d51a..3268ef5f616c 100644 --- a/doc/src/parser/item.rs +++ b/doc/src/parser/item.rs @@ -95,6 +95,7 @@ impl ParseItem { as_inner_source!(pub fn as_contract(&self, Contract) -> ContractDefinition); as_inner_source!(pub fn as_variable(&self, Variable) -> VariableDefinition); + as_inner_source!(pub fn as_function(&self, Function) -> FunctionDefinition); } /// A wrapper type around pt token. diff --git a/doc/src/preprocessor/contract_inheritance.rs b/doc/src/preprocessor/contract_inheritance.rs index cdf4a27b0434..7581b04d4371 100644 --- a/doc/src/preprocessor/contract_inheritance.rs +++ b/doc/src/preprocessor/contract_inheritance.rs @@ -1,5 +1,5 @@ use super::{Preprocessor, PreprocessorId}; -use crate::{Document, ParseSource, PreprocessorOutput}; +use crate::{document::DocumentContent, Document, ParseSource, PreprocessorOutput}; use std::{collections::HashMap, path::PathBuf}; /// [ContractInheritance] preprocessor id. @@ -21,7 +21,7 @@ impl Preprocessor for ContractInheritance { fn preprocess(&self, documents: Vec) -> Result, eyre::Error> { for document in documents.iter() { - for item in document.items.iter() { + if let DocumentContent::Single(ref item) = document.content { if let ParseSource::Contract(ref contract) = item.source { let mut links = HashMap::default(); @@ -49,7 +49,7 @@ impl Preprocessor for ContractInheritance { impl ContractInheritance { fn try_link_base<'a>(&self, base: &str, documents: &Vec) -> Option { for candidate in documents { - for item in candidate.items.iter() { + if let DocumentContent::Single(ref item) = candidate.content { if let ParseSource::Contract(ref contract) = item.source { if base == contract.name.name { return Some(candidate.target_path.clone()) diff --git a/doc/src/writer/as_doc.rs b/doc/src/writer/as_doc.rs index e37d34048d35..ccdccd4d665e 100644 --- a/doc/src/writer/as_doc.rs +++ b/doc/src/writer/as_doc.rs @@ -1,8 +1,11 @@ use std::path::Path; use crate::{ - document::read_context, parser::ParseSource, writer::BufWriter, CommentTag, Comments, - CommentsRef, Document, Markdown, PreprocessorOutput, CONTRACT_INHERITANCE_ID, + document::{read_context, DocumentContent}, + parser::ParseSource, + writer::BufWriter, + AsCode, CommentTag, Comments, CommentsRef, Document, Markdown, PreprocessorOutput, + CONTRACT_INHERITANCE_ID, }; use itertools::Itertools; use solang_parser::pt::Base; @@ -63,190 +66,229 @@ impl AsDoc for Document { fn as_doc(&self) -> AsDocResult { let mut writer = BufWriter::default(); - // Handle constant files - if self.items.iter().all(|item| matches!(item.source, ParseSource::Variable(_))) { - writer.write_title("Constants")?; - - for item in self.items.iter() { - let var = item.as_variable().unwrap(); - writer.write_heading(&var.name.name)?; - writer.write_section(var, &item.comments)?; + match &self.content { + DocumentContent::OverloadedFunctions(items) => { + writer + .write_title(&format!("Function {}", items.first().unwrap().source.ident()))?; + + for item in items.iter() { + let func = item.as_function().unwrap(); + let mut heading = item.source.ident(); + if !func.params.is_empty() { + heading.push_str(&format!( + "({})", + func.params + .iter() + .map(|p| p.1.as_ref().map(|p| p.ty.as_code()).unwrap_or_default()) + .join(", ") + )); + } + writer.write_heading(&heading)?; + writer.write_section(func, &item.comments)?; + } } + DocumentContent::Constants(items) => { + writer.write_title("Constants")?; - return Ok(writer.finish()) - } - - for item in self.items.iter() { - match &item.source { - ParseSource::Contract(contract) => { - writer.write_title(&contract.name.name)?; - - if !contract.base.is_empty() { - writer.write_bold("Inherits:")?; - - let mut bases = vec![]; - let linked = - read_context!(self, CONTRACT_INHERITANCE_ID, ContractInheritance); - for base in contract.base.iter() { - let base_doc = base.as_doc()?; - let base_ident = &base.name.identifiers.last().unwrap().name; - bases.push( - linked - .as_ref() - .and_then(|l| { - l.get(base_ident).map(|path| { - let path = Path::new("/").join( - // TODO: move to func - path.strip_prefix("docs/src").ok().unwrap_or(path), - ); - Markdown::Link(&base_doc, &path.display().to_string()) + for item in items.iter() { + let var = item.as_variable().unwrap(); + writer.write_heading(&var.name.name)?; + writer.write_section(var, &item.comments)?; + } + } + DocumentContent::Single(item) => { + match &item.source { + ParseSource::Contract(contract) => { + writer.write_title(&contract.name.name)?; + + if !contract.base.is_empty() { + writer.write_bold("Inherits:")?; + + let mut bases = vec![]; + let linked = + read_context!(self, CONTRACT_INHERITANCE_ID, ContractInheritance); + for base in contract.base.iter() { + let base_doc = base.as_doc()?; + let base_ident = &base.name.identifiers.last().unwrap().name; + bases.push( + linked + .as_ref() + .and_then(|l| { + l.get(base_ident).map(|path| { + let path = Path::new("/").join( + // TODO: move to func + path.strip_prefix("docs/src") + .ok() + .unwrap_or(path), + ); + Markdown::Link( + &base_doc, + &path.display().to_string(), + ) .as_doc() + }) }) - }) - .transpose()? - .unwrap_or(base_doc), - ) - } + .transpose()? + .unwrap_or(base_doc), + ) + } - writer.writeln_raw(bases.join(", "))?; - writer.writeln()?; - } - - writer.writeln_doc(&item.comments)?; - - if let Some(state_vars) = item.variables() { - writer.write_subtitle("State Variables")?; - state_vars.into_iter().try_for_each(|(item, comments)| { - writer.write_heading(&item.name.name)?; - writer.write_section(item, comments)?; - writer.writeln() - })?; - } - - if let Some(funcs) = item.functions() { - writer.write_subtitle("Functions")?; - funcs.into_iter().try_for_each(|(func, comments)| { - // Write function name - let func_name = func - .name - .as_ref() - .map_or(func.ty.to_string(), |n| n.name.to_owned()); - writer.write_heading(&func_name)?; + writer.writeln_raw(bases.join(", "))?; writer.writeln()?; + } - // Write function docs - writer.writeln_doc( - comments.exclude_tags(&[CommentTag::Param, CommentTag::Return]), - )?; + writer.writeln_doc(&item.comments)?; - // Write function header - writer.write_code(func)?; + if let Some(state_vars) = item.variables() { + writer.write_subtitle("State Variables")?; + state_vars.into_iter().try_for_each(|(item, comments)| { + writer.write_heading(&item.name.name)?; + writer.write_section(item, comments)?; + writer.writeln() + })?; + } - // Write function parameter comments in a table - let params = - func.params.iter().filter_map(|p| p.1.as_ref()).collect::>(); - writer.try_write_param_table(CommentTag::Param, ¶ms, comments)?; + if let Some(funcs) = item.functions() { + writer.write_subtitle("Functions")?; + funcs.into_iter().try_for_each(|(func, comments)| { + // Write function name + let func_name = func + .name + .as_ref() + .map_or(func.ty.to_string(), |n| n.name.to_owned()); + writer.write_heading(&func_name)?; + writer.writeln()?; + + // Write function docs + writer.writeln_doc( + comments.exclude_tags(&[CommentTag::Param, CommentTag::Return]), + )?; + + // Write function header + writer.write_code(func)?; + + // Write function parameter comments in a table + let params = func + .params + .iter() + .filter_map(|p| p.1.as_ref()) + .collect::>(); + writer.try_write_param_table( + CommentTag::Param, + ¶ms, + comments, + )?; + + // Write function parameter comments in a table + let returns = func + .returns + .iter() + .filter_map(|p| p.1.as_ref()) + .collect::>(); + writer.try_write_param_table( + CommentTag::Return, + &returns, + comments, + )?; + + writer.writeln()?; + + Ok::<(), std::fmt::Error>(()) + })?; + } - // Write function parameter comments in a table - let returns = func - .returns - .iter() - .filter_map(|p| p.1.as_ref()) - .collect::>(); - writer.try_write_param_table(CommentTag::Return, &returns, comments)?; + if let Some(events) = item.events() { + writer.write_subtitle("Events")?; + events.into_iter().try_for_each(|(item, comments)| { + writer.write_heading(&item.name.name)?; + writer.write_section(item, comments) + })?; + } - writer.writeln()?; + if let Some(errors) = item.errors() { + writer.write_subtitle("Errors")?; + errors.into_iter().try_for_each(|(item, comments)| { + writer.write_heading(&item.name.name)?; + writer.write_section(item, comments) + })?; + } - Ok::<(), std::fmt::Error>(()) - })?; - } + if let Some(structs) = item.structs() { + writer.write_subtitle("Structs")?; + structs.into_iter().try_for_each(|(item, comments)| { + writer.write_heading(&item.name.name)?; + writer.write_section(item, comments) + })?; + } - if let Some(events) = item.events() { - writer.write_subtitle("Events")?; - events.into_iter().try_for_each(|(item, comments)| { - writer.write_heading(&item.name.name)?; - writer.write_section(item, comments) - })?; + if let Some(enums) = item.enums() { + writer.write_subtitle("Enums")?; + enums.into_iter().try_for_each(|(item, comments)| { + writer.write_heading(&item.name.name)?; + writer.write_section(item, comments) + })?; + } } - - if let Some(errors) = item.errors() { - writer.write_subtitle("Errors")?; - errors.into_iter().try_for_each(|(item, comments)| { - writer.write_heading(&item.name.name)?; - writer.write_section(item, comments) - })?; + ParseSource::Variable(var) => { + writer.write_title(&var.name.name)?; + writer.write_section(var, &item.comments)?; } - - if let Some(structs) = item.structs() { - writer.write_subtitle("Structs")?; - structs.into_iter().try_for_each(|(item, comments)| { - writer.write_heading(&item.name.name)?; - writer.write_section(item, comments) - })?; + ParseSource::Event(event) => { + writer.write_title(&event.name.name)?; + writer.write_section(event, &item.comments)?; + } + ParseSource::Error(error) => { + writer.write_title(&error.name.name)?; + writer.write_section(error, &item.comments)?; + } + ParseSource::Struct(structure) => { + writer.write_title(&structure.name.name)?; + writer.write_section(structure, &item.comments)?; } + ParseSource::Enum(enumerable) => { + writer.write_title(&enumerable.name.name)?; + writer.write_section(enumerable, &item.comments)?; + } + ParseSource::Type(ty) => { + writer.write_title(&ty.name.name)?; + writer.write_section(ty, &item.comments)?; + } + ParseSource::Function(func) => { + // TODO: cleanup + // Write function name + let func_name = + func.name.as_ref().map_or(func.ty.to_string(), |n| n.name.to_owned()); + writer.write_heading(&func_name)?; + writer.writeln()?; + + // Write function docs + writer.writeln_doc( + item.comments.exclude_tags(&[CommentTag::Param, CommentTag::Return]), + )?; + + // Write function header + writer.write_code(func)?; + + // Write function parameter comments in a table + let params = + func.params.iter().filter_map(|p| p.1.as_ref()).collect::>(); + writer.try_write_param_table(CommentTag::Param, ¶ms, &item.comments)?; - if let Some(enums) = item.enums() { - writer.write_subtitle("Enums")?; - enums.into_iter().try_for_each(|(item, comments)| { - writer.write_heading(&item.name.name)?; - writer.write_section(item, comments) - })?; + // Write function parameter comments in a table + let returns = + func.returns.iter().filter_map(|p| p.1.as_ref()).collect::>(); + writer.try_write_param_table( + CommentTag::Return, + &returns, + &item.comments, + )?; + + writer.writeln()?; } } - ParseSource::Variable(var) => { - writer.write_title(&var.name.name)?; - writer.write_section(var, &item.comments)?; - } - ParseSource::Event(event) => { - writer.write_title(&event.name.name)?; - writer.write_section(event, &item.comments)?; - } - ParseSource::Error(error) => { - writer.write_title(&error.name.name)?; - writer.write_section(error, &item.comments)?; - } - ParseSource::Struct(structure) => { - writer.write_title(&structure.name.name)?; - writer.write_section(structure, &item.comments)?; - } - ParseSource::Enum(enumerable) => { - writer.write_title(&enumerable.name.name)?; - writer.write_section(enumerable, &item.comments)?; - } - ParseSource::Type(ty) => { - writer.write_title(&ty.name.name)?; - writer.write_section(ty, &item.comments)?; - } - ParseSource::Function(func) => { - // TODO: cleanup - // Write function name - let func_name = - func.name.as_ref().map_or(func.ty.to_string(), |n| n.name.to_owned()); - writer.write_heading(&func_name)?; - writer.writeln()?; - - // Write function docs - writer.writeln_doc( - item.comments.exclude_tags(&[CommentTag::Param, CommentTag::Return]), - )?; - - // Write function header - writer.write_code(func)?; - - // Write function parameter comments in a table - let params = - func.params.iter().filter_map(|p| p.1.as_ref()).collect::>(); - writer.try_write_param_table(CommentTag::Param, ¶ms, &item.comments)?; - - // Write function parameter comments in a table - let returns = - func.returns.iter().filter_map(|p| p.1.as_ref()).collect::>(); - writer.try_write_param_table(CommentTag::Return, &returns, &item.comments)?; - - writer.writeln()?; - } - }; - } + } + DocumentContent::Empty => (), + }; Ok(writer.finish()) } From 2ff069305c2f510c796f8e189907d4090939c9f3 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Mon, 9 Jan 2023 15:34:20 +0200 Subject: [PATCH 37/67] change case --- doc/src/writer/as_doc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/writer/as_doc.rs b/doc/src/writer/as_doc.rs index ccdccd4d665e..ea46e394ef2e 100644 --- a/doc/src/writer/as_doc.rs +++ b/doc/src/writer/as_doc.rs @@ -69,7 +69,7 @@ impl AsDoc for Document { match &self.content { DocumentContent::OverloadedFunctions(items) => { writer - .write_title(&format!("Function {}", items.first().unwrap().source.ident()))?; + .write_title(&format!("function {}", items.first().unwrap().source.ident()))?; for item in items.iter() { let func = item.as_function().unwrap(); From 13ba1aded5f5083e60abce4e4d9956af74feb4fa Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Mon, 9 Jan 2023 15:38:56 +0200 Subject: [PATCH 38/67] change case --- doc/src/builder.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/doc/src/builder.rs b/doc/src/builder.rs index db7d9b3e5edb..b383ededbc23 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -104,7 +104,6 @@ impl DocBuilder { .into_iter() .partition(|item| !matches!(item.source, ParseSource::Variable(_))); - // TODO: group overloaded functions // Attempt to group overloaded top-level functions let mut remaining = Vec::with_capacity(items.len()); let mut funcs: HashMap> = HashMap::default(); @@ -151,8 +150,8 @@ impl DocBuilder { let identity = match filestem { Some(stem) if stem.to_lowercase().contains("constants") => stem.to_owned(), - Some(stem) => format!("{stem} Constants"), - None => "Constants".to_owned(), + Some(stem) => format!("{stem} constants"), + None => "constants".to_owned(), }; files.push( From 34af12bbc2db32f1e16b20f3856a138abf15f336 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Tue, 10 Jan 2023 09:29:51 +0200 Subject: [PATCH 39/67] inheritdoc preprocessor --- cli/src/cmd/forge/doc.rs | 3 +- doc/src/document.rs | 8 ++- doc/src/parser/comment.rs | 66 ++++++++++++++++++---- doc/src/parser/item.rs | 2 +- doc/src/parser/mod.rs | 1 + doc/src/preprocessor/inheritdoc.rs | 89 ++++++++++++++++++++++++++++++ doc/src/preprocessor/mod.rs | 8 ++- doc/src/writer/as_doc.rs | 22 ++++++-- 8 files changed, 177 insertions(+), 22 deletions(-) create mode 100644 doc/src/preprocessor/inheritdoc.rs diff --git a/cli/src/cmd/forge/doc.rs b/cli/src/cmd/forge/doc.rs index 8c4f9c433b21..181d11091fb8 100644 --- a/cli/src/cmd/forge/doc.rs +++ b/cli/src/cmd/forge/doc.rs @@ -1,6 +1,6 @@ use crate::cmd::Cmd; use clap::{Parser, ValueHint}; -use forge_doc::{ContractInheritance, DocBuilder, Server}; +use forge_doc::{ContractInheritance, DocBuilder, Inheritdoc, Server}; use foundry_config::{find_project_root_path, load_config_with_root}; use std::path::PathBuf; @@ -41,6 +41,7 @@ impl Cmd for DocArgs { .with_out(out.clone()) .with_title(config.doc.title.clone()) .with_preprocessor(ContractInheritance) + .with_preprocessor(Inheritdoc) .build()?; if self.serve { diff --git a/doc/src/document.rs b/doc/src/document.rs index bbc54e74111e..a736fc01c06b 100644 --- a/doc/src/document.rs +++ b/doc/src/document.rs @@ -60,12 +60,14 @@ impl Document { } } -/// TODO: docs +/// Read the preprocessor output variant from document context. +/// Returns [None] if there is no output. macro_rules! read_context { ($doc: expr, $id: expr, $variant: ident) => { - $doc.get_from_context($id).map(|out| match out { + $doc.get_from_context($id).and_then(|out| match out { // Only a single variant is matched. Otherwise the code is invalid. - PreprocessorOutput::$variant(inner) => inner, + PreprocessorOutput::$variant(inner) => Some(inner), + _ => None, }) }; } diff --git a/doc/src/parser/comment.rs b/doc/src/parser/comment.rs index 284510ddad8d..7f72c70cb5e9 100644 --- a/doc/src/parser/comment.rs +++ b/doc/src/parser/comment.rs @@ -1,6 +1,6 @@ -use derive_more::Deref; +use derive_more::{Deref, DerefMut}; use solang_parser::doccomment::DocCommentTag; -use std::str::FromStr; +use std::{collections::HashMap, str::FromStr}; /// The natspec comment tag explaining the purpose of the comment. /// See: https://docs.soliditylang.org/en/v0.8.17/natspec-format.html#tags. @@ -48,7 +48,7 @@ impl FromStr for CommentTag { /// The natspec documentation comment. /// https://docs.soliditylang.org/en/v0.8.17/natspec-format.html -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Clone, Debug)] pub struct Comment { /// The doc comment tag. pub tag: CommentTag, @@ -94,24 +94,51 @@ impl TryFrom for Comment { } /// The collection of natspec [Comment] items. -#[derive(Deref, PartialEq, Default, Debug)] +#[derive(Deref, DerefMut, PartialEq, Default, Clone, Debug)] pub struct Comments(Vec); /// Forward the [Comments] function implementation to the [CommentsRef] /// reference type. macro_rules! ref_fn { - ($vis:vis fn $name:ident(&self, $arg:ty)) => { + ($vis:vis fn $name:ident(&self$(, )?$($arg_name:ident: $arg:ty),*) -> $ret:ty) => { /// Forward the function implementation to [CommentsRef] reference type. - $vis fn $name<'a>(&'a self, arg: $arg) -> CommentsRef<'a> { - CommentsRef::from(self).$name(arg) + $vis fn $name<'a>(&'a self, $($arg_name: $arg),*) -> $ret { + CommentsRef::from(self).$name($($arg_name),*) } }; } impl Comments { - ref_fn!(pub fn include_tag(&self, CommentTag)); - ref_fn!(pub fn include_tags(&self, &[CommentTag])); - ref_fn!(pub fn exclude_tags(&self, &[CommentTag])); + ref_fn!(pub fn include_tag(&self, tag: CommentTag) -> CommentsRef<'_>); + ref_fn!(pub fn include_tags(&self, tags: &[CommentTag]) -> CommentsRef<'_>); + ref_fn!(pub fn exclude_tags(&self, tags: &[CommentTag]) -> CommentsRef<'_>); + ref_fn!(pub fn contains_tag(&self, tag: &Comment) -> bool); + ref_fn!(pub fn find_inheritdoc_base(&self) -> Option<&'_ str>); + + /// Attempt to lookup + /// + /// Merges two comments collections by inserting [CommentTag] from the second collection + /// into the first unless they are present. + pub fn merge_inheritdoc( + &self, + ident: &str, + inheritdocs: Option>, + ) -> Comments { + let mut result = Comments(Vec::from_iter(self.iter().cloned())); + + if let (Some(inheritdocs), Some(base)) = (inheritdocs, self.find_inheritdoc_base()) { + let key = format!("{base}.{ident}"); + if let Some(other) = inheritdocs.get(&key) { + for comment in other.iter() { + if !result.contains_tag(comment) { + result.push(comment.clone()); + } + } + } + } + + result + } } impl TryFrom> for Comments { @@ -143,6 +170,25 @@ impl<'a> CommentsRef<'a> { // Cloning only references here CommentsRef(self.iter().cloned().filter(|c| !tags.contains(&c.tag)).collect()) } + + /// Check if the collection contains a target comment. + pub fn contains_tag(&self, target: &Comment) -> bool { + self.iter().any(|c| match (&c.tag, &target.tag) { + (CommentTag::Inheritdoc, CommentTag::Inheritdoc) => c.value == target.value, + (CommentTag::Param, CommentTag::Param) | (CommentTag::Return, CommentTag::Return) => { + c.split_first_word().map(|(name, _)| name) == + target.split_first_word().map(|(name, _)| name) + } + (tag1, tag2) => tag1 == tag2, + }) + } + + /// Find an [CommentTag::Inheritdoc] comment and extract the base. + fn find_inheritdoc_base(&self) -> Option<&'a str> { + self.iter() + .find(|c| matches!(c.tag, CommentTag::Inheritdoc)) + .and_then(|c| c.value.split_whitespace().next()) + } } impl<'a> From<&'a Comments> for CommentsRef<'a> { diff --git a/doc/src/parser/item.rs b/doc/src/parser/item.rs index 3268ef5f616c..196852c8c3e9 100644 --- a/doc/src/parser/item.rs +++ b/doc/src/parser/item.rs @@ -99,7 +99,7 @@ impl ParseItem { } /// A wrapper type around pt token. -#[derive(Debug, PartialEq)] +#[derive(PartialEq, Debug)] pub enum ParseSource { /// Source contract definition. Contract(Box), diff --git a/doc/src/parser/mod.rs b/doc/src/parser/mod.rs index 3ed1e2320252..41efe0d4e0f7 100644 --- a/doc/src/parser/mod.rs +++ b/doc/src/parser/mod.rs @@ -14,6 +14,7 @@ use solang_parser::{ pub mod error; use error::{ParserError, ParserResult}; +/// Parser item. mod item; pub use item::{ParseItem, ParseSource}; diff --git a/doc/src/preprocessor/inheritdoc.rs b/doc/src/preprocessor/inheritdoc.rs new file mode 100644 index 000000000000..540d084c6c8e --- /dev/null +++ b/doc/src/preprocessor/inheritdoc.rs @@ -0,0 +1,89 @@ +use std::collections::HashMap; + +use super::{Preprocessor, PreprocessorId}; +use crate::{ + document::DocumentContent, Comments, Document, ParseItem, ParseSource, PreprocessorOutput, +}; + +/// [ContractInheritance] preprocessor id. +pub const INHERITDOC_ID: PreprocessorId = PreprocessorId("inheritdoc"); + +/// The inheritdoc preprocessor. +/// +/// This preprocessor writes to [Document]'s context. +#[derive(Debug)] +pub struct Inheritdoc; + +impl Preprocessor for Inheritdoc { + fn id(&self) -> PreprocessorId { + INHERITDOC_ID + } + + fn preprocess(&self, documents: Vec) -> Result, eyre::Error> { + for document in documents.iter() { + if let DocumentContent::Single(ref item) = document.content { + let context = self.visit_item(item, &documents); + if !context.is_empty() { + document.add_context(self.id(), PreprocessorOutput::Inheritdoc(context)); + } + } + } + + Ok(documents) + } +} + +impl Inheritdoc { + fn visit_item(&self, item: &ParseItem, documents: &Vec) -> HashMap { + let mut context = HashMap::default(); + + // Match for the item first. + let matched = item + .comments + .find_inheritdoc_base() + .and_then(|base| self.try_match_inheritdoc(base, &item.source, documents)); + if let Some((key, comments)) = matched { + context.insert(key, comments); + } + + // Match item's children. + for ch in item.children.iter() { + let matched = ch + .comments + .find_inheritdoc_base() + .and_then(|base| self.try_match_inheritdoc(base, &ch.source, documents)); + if let Some((key, comments)) = matched { + context.insert(key, comments); + } + } + + context + } + + fn try_match_inheritdoc( + &self, + base: &str, + source: &ParseSource, + documents: &Vec, + ) -> Option<(String, Comments)> { + for candidate in documents { + if let DocumentContent::Single(ref item) = candidate.content { + if let ParseSource::Contract(ref contract) = item.source { + if base == contract.name.name { + // Not matched for the contract because it's a noop + // https://docs.soliditylang.org/en/v0.8.17/natspec-format.html#tags + + for children in item.children.iter() { + // TODO: improve matching logic + if source.ident() == children.source.ident() { + let key = format!("{}.{}", base, source.ident()); + return Some((key, children.comments.clone())) + } + } + } + } + } + } + None + } +} diff --git a/doc/src/preprocessor/mod.rs b/doc/src/preprocessor/mod.rs index 4b364a7487e2..0e1d262299ca 100644 --- a/doc/src/preprocessor/mod.rs +++ b/doc/src/preprocessor/mod.rs @@ -1,11 +1,14 @@ //! Module containing documentation preprocessors. -use crate::Document; +use crate::{Comments, Document}; use std::{collections::HashMap, fmt::Debug, path::PathBuf}; mod contract_inheritance; pub use contract_inheritance::{ContractInheritance, CONTRACT_INHERITANCE_ID}; +mod inheritdoc; +pub use inheritdoc::{Inheritdoc, INHERITDOC_ID}; + /// The preprocessor id. #[derive(Debug, Eq, Hash, PartialEq)] pub struct PreprocessorId(&'static str); @@ -18,6 +21,9 @@ pub enum PreprocessorOutput { /// The contract inheritance output. /// The map of contract base idents to the path of the base contract. ContractInheritance(HashMap), + /// The inheritdoc output. + /// The map of inherited item keys to their comments. + Inheritdoc(HashMap), } /// Trait for preprocessing and/or modifying existing documents diff --git a/doc/src/writer/as_doc.rs b/doc/src/writer/as_doc.rs index ea46e394ef2e..cfc005b6c66b 100644 --- a/doc/src/writer/as_doc.rs +++ b/doc/src/writer/as_doc.rs @@ -5,7 +5,7 @@ use crate::{ parser::ParseSource, writer::BufWriter, AsCode, CommentTag, Comments, CommentsRef, Document, Markdown, PreprocessorOutput, - CONTRACT_INHERITANCE_ID, + CONTRACT_INHERITANCE_ID, INHERITDOC_ID, }; use itertools::Itertools; use solang_parser::pt::Base; @@ -116,7 +116,6 @@ impl AsDoc for Document { .and_then(|l| { l.get(base_ident).map(|path| { let path = Path::new("/").join( - // TODO: move to func path.strip_prefix("docs/src") .ok() .unwrap_or(path), @@ -142,8 +141,13 @@ impl AsDoc for Document { if let Some(state_vars) = item.variables() { writer.write_subtitle("State Variables")?; state_vars.into_iter().try_for_each(|(item, comments)| { + let comments = comments.merge_inheritdoc( + &item.name.name, + read_context!(self, INHERITDOC_ID, Inheritdoc), + ); + writer.write_heading(&item.name.name)?; - writer.write_section(item, comments)?; + writer.write_section(item, &comments)?; writer.writeln() })?; } @@ -151,16 +155,22 @@ impl AsDoc for Document { if let Some(funcs) = item.functions() { writer.write_subtitle("Functions")?; funcs.into_iter().try_for_each(|(func, comments)| { - // Write function name let func_name = func .name .as_ref() .map_or(func.ty.to_string(), |n| n.name.to_owned()); + let comments = comments.merge_inheritdoc( + &func_name, + read_context!(self, INHERITDOC_ID, Inheritdoc), + ); + + // Write function name writer.write_heading(&func_name)?; writer.writeln()?; // Write function docs writer.writeln_doc( + // TODO: think about multiple inheritdocs comments.exclude_tags(&[CommentTag::Param, CommentTag::Return]), )?; @@ -176,7 +186,7 @@ impl AsDoc for Document { writer.try_write_param_table( CommentTag::Param, ¶ms, - comments, + &comments, )?; // Write function parameter comments in a table @@ -188,7 +198,7 @@ impl AsDoc for Document { writer.try_write_param_table( CommentTag::Return, &returns, - comments, + &comments, )?; writer.writeln()?; From 82f51c43e4467dd04e8c20053ade6f345a3b9397 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Tue, 10 Jan 2023 09:32:36 +0200 Subject: [PATCH 40/67] add docs to gitignore --- cli/assets/.gitignoreTemplate | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cli/assets/.gitignoreTemplate b/cli/assets/.gitignoreTemplate index 3269660cc731..85198aaa55b8 100644 --- a/cli/assets/.gitignoreTemplate +++ b/cli/assets/.gitignoreTemplate @@ -7,5 +7,8 @@ out/ /broadcast/*/31337/ /broadcast/**/dry-run/ +# Docs +docs/ + # Dotenv file .env From 0470b325e2ca781db80f7a4661da490e675250b5 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Tue, 10 Jan 2023 09:33:53 +0200 Subject: [PATCH 41/67] rename root readme to home --- doc/src/builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/builder.rs b/doc/src/builder.rs index b383ededbc23..4ead46b21246 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -216,7 +216,7 @@ impl DocBuilder { // Write summary and section readmes let mut summary = BufWriter::default(); summary.write_title("Summary")?; - summary.write_link_list_item("README", Self::README, 0)?; + summary.write_link_list_item("Home", Self::README, 0)?; self.write_summary_section(&mut summary, &documents.iter().collect::>(), None, 0)?; fs::write(out_dir_src.join(Self::SUMMARY), summary.finish())?; From 4dd1868a76827ff2d9f4e41cbbe9cc51acc8494b Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Tue, 10 Jan 2023 09:35:45 +0200 Subject: [PATCH 42/67] fallback to root readme --- doc/src/builder.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/src/builder.rs b/doc/src/builder.rs index 4ead46b21246..5ae44fc00c1f 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -204,8 +204,11 @@ impl DocBuilder { // Write readme content if any let readme_content = { let src_readme = self.sources.join(Self::README); + let root_readme = self.root.join(Self::README); if src_readme.exists() { fs::read_to_string(src_readme)? + } else if root_readme.exists() { + fs::read_to_string(root_readme)? } else { String::new() } From 9c87e02f3c8c4737bfea85f92f918ccad3c96fe2 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Tue, 10 Jan 2023 09:40:44 +0200 Subject: [PATCH 43/67] render param name & type as code --- doc/src/writer/markdown.rs | 3 +++ doc/src/writer/writer.rs | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/doc/src/writer/markdown.rs b/doc/src/writer/markdown.rs index 2e0d6f9d2a40..0eb947e3e121 100644 --- a/doc/src/writer/markdown.rs +++ b/doc/src/writer/markdown.rs @@ -13,6 +13,8 @@ pub enum Markdown<'a> { Bold(&'a str), /// Link item. Link(&'a str, &'a str), + /// Code item. + Code(&'a str), /// Code block item. CodeBlock(&'a str, &'a str), } @@ -25,6 +27,7 @@ impl<'a> AsDoc for Markdown<'a> { Self::H3(val) => format!("### {val}"), Self::Bold(val) => format!("**{val}**"), Self::Link(val, link) => format!("[{val}]({link})"), + Self::Code(val) => format!("`{val}`"), Self::CodeBlock(lang, val) => format!("```{lang}\n{val}\n```"), }; Ok(doc) diff --git a/doc/src/writer/writer.rs b/doc/src/writer/writer.rs index 5eb0c25ac8df..84e52cc363ac 100644 --- a/doc/src/writer/writer.rs +++ b/doc/src/writer/writer.rs @@ -141,8 +141,8 @@ impl BufWriter { } let row = [ - param_name.unwrap_or_else(|| "".to_owned()), - param.ty.as_code(), + Markdown::Code(¶m_name.unwrap_or_else(|| "".to_owned())).as_doc()?, + Markdown::Code(¶m.ty.as_code()).as_doc()?, comment.unwrap_or_default(), ]; self.write_piped(&row.join("|"))?; From 5be8295c38b6d1aecfd1366e19bd60c5f29c81cf Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Tue, 10 Jan 2023 15:19:28 +0200 Subject: [PATCH 44/67] format code with formatter --- cli/src/cmd/forge/doc.rs | 1 + doc/src/builder.rs | 19 +++++++++++---- doc/src/document.rs | 3 +++ doc/src/parser/error.rs | 10 +++++++- doc/src/parser/item.rs | 51 ++++++++++++++++++++++++++++++++++++---- doc/src/parser/mod.rs | 31 +++++++++++++++++------- doc/src/writer/as_doc.rs | 44 +++++++++++++++++----------------- doc/src/writer/writer.rs | 10 ++++---- fmt/src/comments.rs | 2 +- fmt/src/formatter.rs | 3 ++- fmt/src/inline_config.rs | 2 +- 11 files changed, 126 insertions(+), 50 deletions(-) diff --git a/cli/src/cmd/forge/doc.rs b/cli/src/cmd/forge/doc.rs index 181d11091fb8..f154df7334ab 100644 --- a/cli/src/cmd/forge/doc.rs +++ b/cli/src/cmd/forge/doc.rs @@ -40,6 +40,7 @@ impl Cmd for DocArgs { DocBuilder::new(root, config.project_paths().sources) .with_out(out.clone()) .with_title(config.doc.title.clone()) + .with_fmt(config.fmt.clone()) .with_preprocessor(ContractInheritance) .with_preprocessor(Inheritdoc) .build()?; diff --git a/doc/src/builder.rs b/doc/src/builder.rs index 5ae44fc00c1f..24dd1a3d52d6 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -1,5 +1,5 @@ use ethers_solc::utils::source_files_iter; -use forge_fmt::Visitable; +use forge_fmt::{FormatterConfig, Visitable}; use itertools::Itertools; use rayon::prelude::*; use std::{ @@ -29,6 +29,8 @@ pub struct DocBuilder { pub title: String, /// The array of preprocessors to apply. pub preprocessors: Vec>, + /// The formatter config. + pub fmt: FormatterConfig, } // TODO: consider using `tfio` @@ -46,6 +48,7 @@ impl DocBuilder { out: Default::default(), title: Default::default(), preprocessors: Default::default(), + fmt: Default::default(), } } @@ -55,12 +58,18 @@ impl DocBuilder { self } - /// Set title on the builder + /// Set title on the builder. pub fn with_title(mut self, title: String) -> Self { self.title = title; self } + /// Set formatter config on the builder. + pub fn with_fmt(mut self, fmt: FormatterConfig) -> Self { + self.fmt = fmt; + self + } + /// Set preprocessors on the builder. pub fn with_preprocessor(mut self, preprocessor: P) -> Self { self.preprocessors.push(Box::new(preprocessor) as Box); @@ -95,8 +104,10 @@ impl DocBuilder { diags ) })?; - let mut doc = Parser::new(comments); - source_unit.visit(&mut doc)?; + let mut doc = Parser::new(comments, source).with_fmt(self.fmt.clone()); + source_unit + .visit(&mut doc) + .map_err(|err| eyre::eyre!("Failed to parse source: {err}"))?; // Split the parsed items on top-level constants and rest. let (items, consts): (Vec, Vec) = doc diff --git a/doc/src/document.rs b/doc/src/document.rs index a736fc01c06b..158b013124ab 100644 --- a/doc/src/document.rs +++ b/doc/src/document.rs @@ -10,6 +10,8 @@ pub struct Document { pub content: DocumentContent, /// The original item path. pub item_path: PathBuf, + /// The original item file content. + pub item_content: String, /// The target path where the document will be written. pub target_path: PathBuf, /// The document display identity. @@ -33,6 +35,7 @@ impl Document { Self { item_path, target_path, + item_content: String::default(), identity: String::default(), content: DocumentContent::Empty, context: Mutex::new(HashMap::default()), diff --git a/doc/src/parser/error.rs b/doc/src/parser/error.rs index ad39f0dc1807..26ffb6256fc6 100644 --- a/doc/src/parser/error.rs +++ b/doc/src/parser/error.rs @@ -1,9 +1,17 @@ +use forge_fmt::FormatterError; use thiserror::Error; /// The parser error. #[derive(Error, Debug)] #[error(transparent)] -pub struct ParserError(#[from] eyre::Error); +pub enum ParserError { + /// Formatter error. + #[error(transparent)] + Formatter(#[from] FormatterError), + /// Internal parser error. + #[error(transparent)] + Internal(#[from] eyre::Error), +} /// The parser result. pub type ParserResult = std::result::Result; diff --git a/doc/src/parser/item.rs b/doc/src/parser/item.rs index 196852c8c3e9..a70c5e0e7402 100644 --- a/doc/src/parser/item.rs +++ b/doc/src/parser/item.rs @@ -1,9 +1,10 @@ +use forge_fmt::{Comments as FmtComments, Formatter, FormatterConfig, InlineConfig, Visitor}; use solang_parser::pt::{ ContractDefinition, EnumDefinition, ErrorDefinition, EventDefinition, FunctionDefinition, StructDefinition, TypeDefinition, VariableDefinition, }; -use crate::Comments; +use crate::{error::ParserResult, Comments}; /// The parsed item. #[derive(PartialEq, Debug)] @@ -14,6 +15,8 @@ pub struct ParseItem { pub comments: Comments, /// Children items. pub children: Vec, + /// Formatted code string. + pub code: String, } /// Defines a method that filters [ParseItem]'s children and returns the source pt token of the @@ -22,9 +25,9 @@ pub struct ParseItem { macro_rules! filter_children_fn { ($vis:vis fn $name:ident(&self, $variant:ident) -> $ret:ty) => { /// Filter children items for [ParseSource::$variant] variants. - $vis fn $name<'a>(&'a self) -> Option> { + $vis fn $name<'a>(&'a self) -> Option> { let items = self.children.iter().filter_map(|item| match item.source { - ParseSource::$variant(ref inner) => Some((inner, &item.comments)), + ParseSource::$variant(ref inner) => Some((inner, &item.comments, &item.code)), _ => None, }); let items = items.collect::>(); @@ -55,7 +58,12 @@ macro_rules! as_inner_source { impl ParseItem { /// Create new instance of [ParseItem]. pub fn new(source: ParseSource) -> Self { - Self { source, comments: Default::default(), children: Default::default() } + Self { + source, + comments: Default::default(), + children: Default::default(), + code: Default::default(), + } } /// Set comments on the [ParseItem]. @@ -70,6 +78,39 @@ impl ParseItem { self } + /// Set formatted code on the [ParseItem]. + pub fn with_code(mut self, source: &str, config: FormatterConfig) -> ParserResult { + let mut code = String::new(); + let mut fmt = Formatter::new( + &mut code, + &source, + FmtComments::default(), + InlineConfig::default(), + config, + ); + + match self.source.clone() { + ParseSource::Contract(mut contract) => { + contract.parts = vec![]; + fmt.visit_contract(&mut contract)? + } + ParseSource::Function(mut func) => { + func.body = None; + fmt.visit_function(&mut func)? + } + ParseSource::Variable(mut var) => fmt.visit_var_definition(&mut var)?, + ParseSource::Event(mut event) => fmt.visit_event(&mut event)?, + ParseSource::Error(mut error) => fmt.visit_error(&mut error)?, + ParseSource::Struct(mut structure) => fmt.visit_struct(&mut structure)?, + ParseSource::Enum(mut enumeration) => fmt.visit_enum(&mut enumeration)?, + ParseSource::Type(mut ty) => fmt.visit_type_definition(&mut ty)?, + }; + + self.code = code; + + Ok(self) + } + /// Format the item's filename. pub fn filename(&self) -> String { let prefix = match self.source { @@ -99,7 +140,7 @@ impl ParseItem { } /// A wrapper type around pt token. -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Clone, Debug)] pub enum ParseSource { /// Source contract definition. Contract(Box), diff --git a/doc/src/parser/mod.rs b/doc/src/parser/mod.rs index 41efe0d4e0f7..7eb15c694e93 100644 --- a/doc/src/parser/mod.rs +++ b/doc/src/parser/mod.rs @@ -1,6 +1,6 @@ //! The parser module. -use forge_fmt::{Visitable, Visitor}; +use forge_fmt::{FormatterConfig, Visitable, Visitor}; use solang_parser::{ doccomment::{parse_doccomments, DocComment}, pt::{ @@ -33,6 +33,10 @@ pub struct Parser { context: ParserContext, /// Parsed results. items: Vec, + /// Source file. + source: String, + /// The formatter config. + fmt: FormatterConfig, } /// [Parser] context. @@ -46,8 +50,14 @@ struct ParserContext { impl Parser { /// Create a new instance of [Parser]. - pub fn new(comments: Vec) -> Self { - Parser { comments, ..Default::default() } + pub fn new(comments: Vec, source: String) -> Self { + Parser { comments, source, ..Default::default() } + } + + /// Set formatter config on the [Parser] + pub fn with_fmt(mut self, fmt: FormatterConfig) -> Self { + self.fmt = fmt; + self } /// Return the parsed items. Consumes the parser. @@ -76,7 +86,7 @@ impl Parser { /// Otherwise the element will be added to a top-level items collection. /// Moves the doc comment pointer to the end location of the child element. fn add_element_to_parent(&mut self, source: ParseSource, loc: Loc) -> ParserResult<()> { - let child = ParseItem { source, comments: self.parse_docs(loc.start())?, children: vec![] }; + let child = self.new_item(source, loc.start())?; if let Some(parent) = self.context.parent.as_mut() { parent.children.push(child); } else { @@ -86,6 +96,12 @@ impl Parser { Ok(()) } + /// Create new [ParseItem] with comments and formatted code. + fn new_item(&mut self, source: ParseSource, loc_start: usize) -> ParserResult { + let docs = self.parse_docs(loc_start)?; + ParseItem::new(source).with_comments(docs).with_code(&self.source, self.fmt.clone()) + } + /// Parse the doc comments from the current start location. fn parse_docs(&mut self, end: usize) -> ParserResult { let mut res = vec![]; @@ -107,9 +123,8 @@ impl Visitor for Parser { match source { SourceUnitPart::ContractDefinition(def) => { // Create new contract parse item. - let source = ParseSource::Contract(def.clone()); - let comments = self.parse_docs(def.loc.start())?; - let contract = ParseItem::new(source).with_comments(comments); + let contract = + self.new_item(ParseSource::Contract(def.clone()), def.loc.start())?; // Move the doc pointer to the contract location start. self.context.doc_start_loc = def.loc.start(); @@ -181,7 +196,7 @@ mod tests { #[inline] fn parse_source(src: &str) -> Vec { let (mut source, comments) = parse(src, 0).expect("failed to parse source"); - let mut doc = Parser::new(comments); + let mut doc = Parser::new(comments, src.to_owned()); // TODO: source.visit(&mut doc).expect("failed to visit source"); doc.items() } diff --git a/doc/src/writer/as_doc.rs b/doc/src/writer/as_doc.rs index cfc005b6c66b..394c2ec7d3a0 100644 --- a/doc/src/writer/as_doc.rs +++ b/doc/src/writer/as_doc.rs @@ -36,8 +36,6 @@ impl<'a> AsDoc for CommentsRef<'a> { fn as_doc(&self) -> AsDocResult { let mut writer = BufWriter::default(); - // TODO: title - let authors = self.include_tag(CommentTag::Author); if !authors.is_empty() { writer.write_bold(&format!("Author{}:", if authors.len() == 1 { "" } else { "s" }))?; @@ -84,7 +82,7 @@ impl AsDoc for Document { )); } writer.write_heading(&heading)?; - writer.write_section(func, &item.comments)?; + writer.write_section(&item.comments, &item.code)?; } } DocumentContent::Constants(items) => { @@ -93,7 +91,7 @@ impl AsDoc for Document { for item in items.iter() { let var = item.as_variable().unwrap(); writer.write_heading(&var.name.name)?; - writer.write_section(var, &item.comments)?; + writer.write_section(&item.comments, &item.code)?; } } DocumentContent::Single(item) => { @@ -140,21 +138,21 @@ impl AsDoc for Document { if let Some(state_vars) = item.variables() { writer.write_subtitle("State Variables")?; - state_vars.into_iter().try_for_each(|(item, comments)| { + state_vars.into_iter().try_for_each(|(item, comments, code)| { let comments = comments.merge_inheritdoc( &item.name.name, read_context!(self, INHERITDOC_ID, Inheritdoc), ); writer.write_heading(&item.name.name)?; - writer.write_section(item, &comments)?; + writer.write_section(&comments, code)?; writer.writeln() })?; } if let Some(funcs) = item.functions() { writer.write_subtitle("Functions")?; - funcs.into_iter().try_for_each(|(func, comments)| { + funcs.into_iter().try_for_each(|(func, comments, code)| { let func_name = func .name .as_ref() @@ -175,7 +173,7 @@ impl AsDoc for Document { )?; // Write function header - writer.write_code(func)?; + writer.write_code(&code)?; // Write function parameter comments in a table let params = func @@ -209,59 +207,59 @@ impl AsDoc for Document { if let Some(events) = item.events() { writer.write_subtitle("Events")?; - events.into_iter().try_for_each(|(item, comments)| { + events.into_iter().try_for_each(|(item, comments, code)| { writer.write_heading(&item.name.name)?; - writer.write_section(item, comments) + writer.write_section(comments, code) })?; } if let Some(errors) = item.errors() { writer.write_subtitle("Errors")?; - errors.into_iter().try_for_each(|(item, comments)| { + errors.into_iter().try_for_each(|(item, comments, code)| { writer.write_heading(&item.name.name)?; - writer.write_section(item, comments) + writer.write_section(comments, code) })?; } if let Some(structs) = item.structs() { writer.write_subtitle("Structs")?; - structs.into_iter().try_for_each(|(item, comments)| { + structs.into_iter().try_for_each(|(item, comments, code)| { writer.write_heading(&item.name.name)?; - writer.write_section(item, comments) + writer.write_section(comments, code) })?; } if let Some(enums) = item.enums() { writer.write_subtitle("Enums")?; - enums.into_iter().try_for_each(|(item, comments)| { + enums.into_iter().try_for_each(|(item, comments, code)| { writer.write_heading(&item.name.name)?; - writer.write_section(item, comments) + writer.write_section(comments, code) })?; } } ParseSource::Variable(var) => { writer.write_title(&var.name.name)?; - writer.write_section(var, &item.comments)?; + writer.write_section(&item.comments, &item.code)?; } ParseSource::Event(event) => { writer.write_title(&event.name.name)?; - writer.write_section(event, &item.comments)?; + writer.write_section(&item.comments, &item.code)?; } ParseSource::Error(error) => { writer.write_title(&error.name.name)?; - writer.write_section(error, &item.comments)?; + writer.write_section(&item.comments, &item.code)?; } ParseSource::Struct(structure) => { writer.write_title(&structure.name.name)?; - writer.write_section(structure, &item.comments)?; + writer.write_section(&item.comments, &item.code)?; } ParseSource::Enum(enumerable) => { writer.write_title(&enumerable.name.name)?; - writer.write_section(enumerable, &item.comments)?; + writer.write_section(&item.comments, &item.code)?; } ParseSource::Type(ty) => { writer.write_title(&ty.name.name)?; - writer.write_section(ty, &item.comments)?; + writer.write_section(&item.comments, &item.code)?; } ParseSource::Function(func) => { // TODO: cleanup @@ -277,7 +275,7 @@ impl AsDoc for Document { )?; // Write function header - writer.write_code(func)?; + writer.write_code(&item.code)?; // Write function parameter comments in a table let params = diff --git a/doc/src/writer/writer.rs b/doc/src/writer/writer.rs index 84e52cc363ac..9131f6b17e91 100644 --- a/doc/src/writer/writer.rs +++ b/doc/src/writer/writer.rs @@ -82,16 +82,14 @@ impl BufWriter { } /// Writes a solidity code block block to the buffer. - pub fn write_code(&mut self, item: T) -> fmt::Result { - let code = item.as_code(); - let block = Markdown::CodeBlock("solidity", &code); - writeln!(self.buf, "{block}") + pub fn write_code(&mut self, code: &str) -> fmt::Result { + writeln!(self.buf, "{}", Markdown::CodeBlock("solidity", &code)) } /// Write an item section to the buffer. First write comments, the item itself as code. - pub fn write_section(&mut self, item: T, comments: &Comments) -> fmt::Result { + pub fn write_section(&mut self, comments: &Comments, code: &str) -> fmt::Result { self.writeln_raw(&comments.as_doc()?)?; - self.write_code(item)?; + self.write_code(code)?; self.writeln() } diff --git a/fmt/src/comments.rs b/fmt/src/comments.rs index 5d94a61e6844..85a3c32d6da8 100644 --- a/fmt/src/comments.rs +++ b/fmt/src/comments.rs @@ -212,7 +212,7 @@ impl CommentWithMetadata { } /// A list of comments -#[derive(Debug, Clone)] +#[derive(Default, Debug, Clone)] pub struct Comments { prefixes: VecDeque, postfixes: VecDeque, diff --git a/fmt/src/formatter.rs b/fmt/src/formatter.rs index 0fe9729e7df4..56b9b728efe9 100644 --- a/fmt/src/formatter.rs +++ b/fmt/src/formatter.rs @@ -69,7 +69,7 @@ macro_rules! bail { // TODO: store context entities as references without copying /// Current context of the Formatter (e.g. inside Contract or Function definition) -#[derive(Default)] +#[derive(Default, Debug)] struct Context { contract: Option, function: Option, @@ -77,6 +77,7 @@ struct Context { } /// A Solidity formatter +#[derive(Debug)] pub struct Formatter<'a, W> { buf: FormatBuffer<&'a mut W>, source: &'a str, diff --git a/fmt/src/inline_config.rs b/fmt/src/inline_config.rs index 2cd32cebf505..b0e3893d50da 100644 --- a/fmt/src/inline_config.rs +++ b/fmt/src/inline_config.rs @@ -63,7 +63,7 @@ impl DisabledRange { /// This is a list of Inline Config items for locations in a source file. This is /// usually acquired by parsing the comments for an `forgefmt:` items. See /// [`Comments::parse_inline_config_items`] for details. -#[derive(Debug)] +#[derive(Default, Debug)] pub struct InlineConfig { disabled_ranges: Vec, } From b3cf4f89127aeda5524c105a98c8b02a1d1098a5 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Tue, 10 Jan 2023 15:28:55 +0200 Subject: [PATCH 45/67] trim down as_code & rename to as_string --- doc/src/lib.rs | 2 +- doc/src/writer/as_doc.rs | 4 +- doc/src/writer/{as_code.rs => as_string.rs} | 245 +++++--------------- doc/src/writer/mod.rs | 4 +- doc/src/writer/writer.rs | 4 +- 5 files changed, 65 insertions(+), 194 deletions(-) rename doc/src/writer/{as_code.rs => as_string.rs} (51%) diff --git a/doc/src/lib.rs b/doc/src/lib.rs index 82a27715707a..284b991e0105 100644 --- a/doc/src/lib.rs +++ b/doc/src/lib.rs @@ -34,4 +34,4 @@ pub use parser::{ pub use preprocessor::*; /// Traits for formatting items into doc output. -pub use writer::{AsCode, AsDoc, AsDocResult, BufWriter, Markdown}; +pub use writer::{AsDoc, AsDocResult, AsString, BufWriter, Markdown}; diff --git a/doc/src/writer/as_doc.rs b/doc/src/writer/as_doc.rs index 394c2ec7d3a0..d81a023e3352 100644 --- a/doc/src/writer/as_doc.rs +++ b/doc/src/writer/as_doc.rs @@ -4,7 +4,7 @@ use crate::{ document::{read_context, DocumentContent}, parser::ParseSource, writer::BufWriter, - AsCode, CommentTag, Comments, CommentsRef, Document, Markdown, PreprocessorOutput, + AsString, CommentTag, Comments, CommentsRef, Document, Markdown, PreprocessorOutput, CONTRACT_INHERITANCE_ID, INHERITDOC_ID, }; use itertools::Itertools; @@ -77,7 +77,7 @@ impl AsDoc for Document { "({})", func.params .iter() - .map(|p| p.1.as_ref().map(|p| p.ty.as_code()).unwrap_or_default()) + .map(|p| p.1.as_ref().map(|p| p.ty.as_string()).unwrap_or_default()) .join(", ") )); } diff --git a/doc/src/writer/as_code.rs b/doc/src/writer/as_string.rs similarity index 51% rename from doc/src/writer/as_code.rs rename to doc/src/writer/as_string.rs index c12d500ba333..6a0d66403580 100644 --- a/doc/src/writer/as_code.rs +++ b/doc/src/writer/as_string.rs @@ -1,76 +1,18 @@ -use std::str::FromStr; - use ethers_core::{types::H160, utils::to_checksum}; use forge_fmt::solang_ext::{AsStr, AttrSortKeyIteratorExt, Operator, OperatorComponents}; use itertools::Itertools; -use solang_parser::pt::{ - EnumDefinition, ErrorDefinition, ErrorParameter, EventDefinition, EventParameter, Expression, - FunctionAttribute, FunctionDefinition, Identifier, IdentifierPath, Loc, Parameter, - StructDefinition, Type, TypeDefinition, VariableAttribute, VariableDeclaration, - VariableDefinition, -}; +use solang_parser::pt::{Expression, FunctionAttribute, IdentifierPath, Loc, Parameter, Type}; +use std::str::FromStr; -// TODO: delegate this logic to [forge_fmt::Formatter] -/// Display Solidity parse tree item as code string. +/// Trait for rendering parse tree items as strings. #[auto_impl::auto_impl(&)] -pub trait AsCode { - /// Formats a parse tree item into a valid Solidity code block. - fn as_code(&self) -> String; +pub trait AsString { + /// Render parse tree item as string. + fn as_string(&self) -> String; } -impl AsCode for VariableDefinition { - fn as_code(&self) -> String { - let ty = self.ty.as_code(); - let mut attrs = self.attrs.iter().attr_sorted().map(|attr| attr.as_code()).join(" "); - if !attrs.is_empty() { - attrs.insert(0, ' '); - } - let name = self.name.name.to_owned(); - let init = self - .initializer - .as_ref() - .map(|init| format!(" = {}", init.as_code())) - .unwrap_or_default(); - format!("{ty}{attrs} {name}{init}") - } -} - -impl AsCode for VariableAttribute { - fn as_code(&self) -> String { - match self { - VariableAttribute::Visibility(visibility) => visibility.to_string(), - VariableAttribute::Constant(_) => "constant".to_owned(), - VariableAttribute::Immutable(_) => "immutable".to_owned(), - VariableAttribute::Override(_, idents) => { - let mut val = "override".to_owned(); - if !idents.is_empty() { - val.push_str(&format!("({})", idents.iter().map(AsCode::as_code).join(", "))); - } - val - } - } - } -} - -impl AsCode for FunctionDefinition { - fn as_code(&self) -> String { - let ty = self.ty.to_string(); - let name = self.name.as_ref().map(|n| format!(" {}", n.name)).unwrap_or_default(); - let params = self.params.as_code(); - let mut attributes = self.attributes.as_code(); - if !attributes.is_empty() { - attributes.insert(0, ' '); - } - let mut returns = self.returns.as_code(); - if !returns.is_empty() { - returns = format!(" returns ({returns})") - } - format!("{ty}{name}({params}){attributes}{returns}") - } -} - -impl AsCode for Expression { - fn as_code(&self) -> String { +impl AsString for Expression { + fn as_string(&self) -> String { match self { Expression::Type(_, ty) => match ty { Type::Address => "address".to_owned(), @@ -84,17 +26,17 @@ impl AsCode for Expression { Type::Int(n) => format!("int{n}"), Type::Uint(n) => format!("uint{n}"), Type::Mapping(_, from, to) => { - format!("mapping({} => {})", from.as_code(), to.as_code()) + format!("mapping({} => {})", from.as_string(), to.as_string()) } Type::Function { params, attributes, returns } => { - let params = params.as_code(); - let mut attributes = attributes.as_code(); + let params = params.as_string(); + let mut attributes = attributes.as_string(); if !attributes.is_empty() { attributes.insert(0, ' '); } let mut returns_str = String::new(); if let Some((params, _attrs)) = returns { - returns_str = params.as_code(); + returns_str = params.as_string(); if !returns_str.is_empty() { returns_str = format!(" returns ({returns_str})") } @@ -105,14 +47,14 @@ impl AsCode for Expression { Expression::Variable(ident) => ident.name.to_owned(), Expression::ArraySubscript(_, expr1, expr2) => format!( "{}[{}]", - expr1.as_code(), - expr2.as_ref().map(|expr| expr.as_code()).unwrap_or_default() + expr1.as_string(), + expr2.as_ref().map(|expr| expr.as_string()).unwrap_or_default() ), Expression::MemberAccess(_, expr, ident) => { - format!("{}.{}", ident.name, expr.as_code()) + format!("{}.{}", ident.name, expr.as_string()) } Expression::Parenthesis(_, expr) => { - format!("({})", expr.as_code()) + format!("({})", expr.as_string()) } Expression::HexNumberLiteral(_, val) => { // ref: https://docs.soliditylang.org/en/latest/types.html?highlight=address%20literal#address-literals @@ -143,7 +85,7 @@ impl AsCode for Expression { vals.iter().map(|val| format!("hex\"{}\"", val.hex)).join(" ") } Expression::ArrayLiteral(_, exprs) => { - format!("[{}]", exprs.iter().map(AsCode::as_code).join(", ")) + format!("[{}]", exprs.iter().map(AsString::as_string).join(", ")) } Expression::RationalNumberLiteral(_, val, fraction, exp) => { let mut val = val.replace('_', ""); @@ -163,10 +105,14 @@ impl AsCode for Expression { val } Expression::FunctionCall(_, expr, exprs) => { - format!("{}({})", expr.as_code(), exprs.iter().map(AsCode::as_code).join(", ")) + format!( + "{}({})", + expr.as_string(), + exprs.iter().map(AsString::as_string).join(", ") + ) } Expression::Unit(_, expr, unit) => { - format!("{} {}", expr.as_code(), unit.as_str()) + format!("{} {}", expr.as_string(), unit.as_str()) } Expression::PreIncrement(..) | Expression::PostIncrement(..) | @@ -204,13 +150,13 @@ impl AsCode for Expression { if spaced { val.insert(0, ' '); } - val.insert_str(0, &left.as_code()); + val.insert_str(0, &left.as_string()); } if let Some(right) = right { if spaced { val.push(' '); } - val.push_str(&right.as_code()) + val.push_str(&right.as_string()) } val @@ -222,30 +168,10 @@ impl AsCode for Expression { } } -impl AsCode for FunctionAttribute { - fn as_code(&self) -> String { - match self { - Self::Mutability(mutability) => mutability.to_string(), - Self::Visibility(visibility) => visibility.to_string(), - Self::Virtual(_) => "virtual".to_owned(), - Self::Immutable(_) => "immutable".to_owned(), - Self::Override(_, idents) => { - let mut val = "override".to_owned(); - if !idents.is_empty() { - val.push_str(&format!("({})", idents.iter().map(AsCode::as_code).join(", "))); - } - val - } - Self::BaseOrModifier(_, _base) => "".to_owned(), // TODO: - Self::NameValue(..) => unreachable!(), - } - } -} - -impl AsCode for Parameter { - fn as_code(&self) -> String { +impl AsString for Parameter { + fn as_string(&self) -> String { [ - Some(self.ty.as_code()), + Some(self.ty.as_string()), self.storage.as_ref().map(|storage| storage.to_string()), self.name.as_ref().map(|name| name.name.clone()), ] @@ -255,102 +181,47 @@ impl AsCode for Parameter { } } -impl AsCode for Vec<(Loc, Option)> { - fn as_code(&self) -> String { +impl AsString for Vec<(Loc, Option)> { + fn as_string(&self) -> String { self.iter() - .map(|(_, param)| param.as_ref().map(AsCode::as_code).unwrap_or_default()) + .map(|(_, param)| param.as_ref().map(AsString::as_string).unwrap_or_default()) .join(", ") } } -impl AsCode for Vec { - fn as_code(&self) -> String { - self.iter().attr_sorted().map(|attr| attr.as_code()).join(" ") - } -} - -impl AsCode for EventDefinition { - fn as_code(&self) -> String { - let name = &self.name.name; - let fields = self.fields.as_code(); - let anonymous = if self.anonymous { " anonymous" } else { "" }; - format!("event {name}({fields}){anonymous}") +impl AsString for Vec { + fn as_string(&self) -> String { + self.iter().attr_sorted().map(|attr| attr.as_string()).join(" ") } } -impl AsCode for EventParameter { - fn as_code(&self) -> String { - let ty = self.ty.as_code(); - let indexed = if self.indexed { " indexed" } else { "" }; - let name = self.name.as_ref().map(|name| name.name.to_owned()).unwrap_or_default(); - format!("{ty}{indexed} {name}") - } -} - -impl AsCode for Vec { - fn as_code(&self) -> String { - self.iter().map(AsCode::as_code).join(", ") - } -} - -impl AsCode for ErrorDefinition { - fn as_code(&self) -> String { - let name = &self.name.name; - let fields = self.fields.as_code(); - format!("error {name}({fields})") - } -} - -impl AsCode for ErrorParameter { - fn as_code(&self) -> String { - let ty = self.ty.as_code(); - let name = self.name.as_ref().map(|name| name.name.to_owned()).unwrap_or_default(); - format!("{ty} {name}") - } -} - -impl AsCode for Vec { - fn as_code(&self) -> String { - self.iter().map(AsCode::as_code).join(", ") +impl AsString for FunctionAttribute { + fn as_string(&self) -> String { + match self { + Self::Mutability(mutability) => mutability.to_string(), + Self::Visibility(visibility) => visibility.to_string(), + Self::Virtual(_) => "virtual".to_owned(), + Self::Immutable(_) => "immutable".to_owned(), + Self::Override(_, idents) => { + let mut val = "override".to_owned(); + if !idents.is_empty() { + val.push_str(&format!( + "({})", + idents.iter().map(AsString::as_string).join(", ") + )); + } + val + } + Self::BaseOrModifier(_, base) => { + base.name.identifiers.iter().map(|i| i.name.to_owned()).join(".") + } + Self::NameValue(..) => unreachable!(), + } } } -impl AsCode for IdentifierPath { - fn as_code(&self) -> String { +impl AsString for IdentifierPath { + fn as_string(&self) -> String { self.identifiers.iter().map(|ident| ident.name.to_owned()).join(".") } } - -impl AsCode for StructDefinition { - fn as_code(&self) -> String { - let name = &self.name.name; - let fields = self.fields.iter().map(AsCode::as_code).join(";\n\t"); - format!("struct {name} {{\n\t{fields};\n}}") - } -} - -impl AsCode for VariableDeclaration { - fn as_code(&self) -> String { - format!("{} {}", self.ty.as_code(), self.name.name) - } -} - -impl AsCode for EnumDefinition { - fn as_code(&self) -> String { - let name = &self.name.name; - let values = self.values.iter().map(AsCode::as_code).join("\n\t"); - format!("enum {name} {{\n\t{values}\n}}") - } -} - -impl AsCode for TypeDefinition { - fn as_code(&self) -> String { - format!("type {} is {}", &self.name.name, self.ty.as_code()) - } -} - -impl AsCode for Identifier { - fn as_code(&self) -> String { - self.name.to_string() - } -} diff --git a/doc/src/writer/mod.rs b/doc/src/writer/mod.rs index 2f909a5de9d2..d4d9f6ab7c09 100644 --- a/doc/src/writer/mod.rs +++ b/doc/src/writer/mod.rs @@ -1,11 +1,11 @@ //! The module for writing and formatting various parse tree items. -mod as_code; mod as_doc; +mod as_string; mod markdown; mod writer; -pub use as_code::AsCode; pub use as_doc::{AsDoc, AsDocResult}; +pub use as_string::AsString; pub use markdown::Markdown; pub use writer::BufWriter; diff --git a/doc/src/writer/writer.rs b/doc/src/writer/writer.rs index 9131f6b17e91..0caac979f560 100644 --- a/doc/src/writer/writer.rs +++ b/doc/src/writer/writer.rs @@ -2,7 +2,7 @@ use itertools::Itertools; use solang_parser::pt::Parameter; use std::fmt::{self, Display, Write}; -use crate::{AsCode, AsDoc, CommentTag, Comments, Markdown}; +use crate::{AsDoc, AsString, CommentTag, Comments, Markdown}; /// The buffered writer. /// Writes various display items into the internal buffer. @@ -140,7 +140,7 @@ impl BufWriter { let row = [ Markdown::Code(¶m_name.unwrap_or_else(|| "".to_owned())).as_doc()?, - Markdown::Code(¶m.ty.as_code()).as_doc()?, + Markdown::Code(¶m.ty.as_string()).as_doc()?, comment.unwrap_or_default(), ]; self.write_piped(&row.join("|"))?; From e722578bd28bcf343b61177deafc140b06b5e355 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Tue, 10 Jan 2023 15:31:24 +0200 Subject: [PATCH 46/67] add link to mdbook config --- doc/static/book.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/static/book.toml b/doc/static/book.toml index f5fe02033526..11c8d042906b 100644 --- a/doc/static/book.toml +++ b/doc/static/book.toml @@ -1,3 +1,4 @@ +# For more configuration see https://rust-lang.github.io/mdBook/format/configuration/index.html [book] src = "src" From 5c770d240d2dd716bc99136940a12b829b0f7f52 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Wed, 11 Jan 2023 10:39:55 +0200 Subject: [PATCH 47/67] add prefix to dir menu entries --- doc/src/builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/builder.rs b/doc/src/builder.rs index 24dd1a3d52d6..a1b29c93635d 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -282,7 +282,7 @@ impl DocBuilder { } else { let summary_path = path.join(Self::README); summary.write_link_list_item( - &title, + &format!("❱ {title}"), &summary_path.display().to_string(), depth - 1, )?; From 33fadfd8f18f896915a22ee8f17aa5d727462784 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Wed, 11 Jan 2023 11:42:13 +0200 Subject: [PATCH 48/67] write dev tags in italics --- Cargo.lock | 1 + doc/Cargo.toml | 1 + doc/src/writer/as_doc.rs | 17 +++++++++++++---- doc/src/writer/markdown.rs | 3 +++ doc/src/writer/writer.rs | 25 +++++++++++++++++-------- 5 files changed, 35 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 16e62a19002b..e2f6047260ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2389,6 +2389,7 @@ dependencies = [ "futures-util", "itertools", "mdbook", + "once_cell", "rayon", "solang-parser", "thiserror", diff --git a/doc/Cargo.toml b/doc/Cargo.toml index c4abd9d4d67c..7369f7a51853 100644 --- a/doc/Cargo.toml +++ b/doc/Cargo.toml @@ -40,6 +40,7 @@ itertools = "0.10.3" toml = "0.5" auto_impl = "1" derive_more = "0.99" +once_cell = "1.13" [dev-dependencies] assert_matches = "1.5.0" diff --git a/doc/src/writer/as_doc.rs b/doc/src/writer/as_doc.rs index d81a023e3352..153bf8f6bb1c 100644 --- a/doc/src/writer/as_doc.rs +++ b/doc/src/writer/as_doc.rs @@ -33,9 +33,11 @@ impl AsDoc for Comments { } impl<'a> AsDoc for CommentsRef<'a> { + // TODO: support other tags fn as_doc(&self) -> AsDocResult { let mut writer = BufWriter::default(); + // Write author tag(s) let authors = self.include_tag(CommentTag::Author); if !authors.is_empty() { writer.write_bold(&format!("Author{}:", if authors.len() == 1 { "" } else { "s" }))?; @@ -43,10 +45,17 @@ impl<'a> AsDoc for CommentsRef<'a> { writer.writeln()?; } - // TODO: other tags - let docs = self.include_tags(&[CommentTag::Dev, CommentTag::Notice]); - for doc in docs.iter() { - writer.writeln_raw(&doc.value)?; + // Write notice tags + let notices = self.include_tag(CommentTag::Notice); + for notice in notices.iter() { + writer.writeln_raw(¬ice.value)?; + writer.writeln()?; + } + + // Write dev tags + let devs = self.include_tag(CommentTag::Dev); + for dev in devs.iter() { + writer.write_italic(&dev.value)?; writer.writeln()?; } diff --git a/doc/src/writer/markdown.rs b/doc/src/writer/markdown.rs index 0eb947e3e121..2e577ad9601c 100644 --- a/doc/src/writer/markdown.rs +++ b/doc/src/writer/markdown.rs @@ -9,6 +9,8 @@ pub enum Markdown<'a> { H2(&'a str), /// H3 heading item. H3(&'a str), + /// Italic item. + Italic(&'a str), /// Bold item. Bold(&'a str), /// Link item. @@ -25,6 +27,7 @@ impl<'a> AsDoc for Markdown<'a> { Self::H1(val) => format!("# {val}"), Self::H2(val) => format!("## {val}"), Self::H3(val) => format!("### {val}"), + Self::Italic(val) => format!("*{val}*"), Self::Bold(val) => format!("**{val}**"), Self::Link(val, link) => format!("[{val}]({link})"), Self::Code(val) => format!("`{val}`"), diff --git a/doc/src/writer/writer.rs b/doc/src/writer/writer.rs index 0caac979f560..05aec70f3aa6 100644 --- a/doc/src/writer/writer.rs +++ b/doc/src/writer/writer.rs @@ -1,9 +1,18 @@ use itertools::Itertools; +use once_cell::sync::Lazy; use solang_parser::pt::Parameter; use std::fmt::{self, Display, Write}; use crate::{AsDoc, AsString, CommentTag, Comments, Markdown}; +/// Solidity language name. +const SOLIDITY: &'static str = "solidity"; + +/// Headers and separator for rendering parameter table. +const PARAM_TABLE_HEADERS: &'static [&'static str] = &["Name", "Type", "Description"]; +static PARAM_TABLE_SEPARATOR: Lazy = + Lazy::new(|| PARAM_TABLE_HEADERS.iter().map(|h| "-".repeat(h.len())).join("|")); + /// The buffered writer. /// Writes various display items into the internal buffer. #[derive(Default, Debug)] @@ -12,8 +21,6 @@ pub struct BufWriter { } impl BufWriter { - const PARAM_TABLE_HEADERS: &'static [&'static str] = &["Name", "Type", "Description"]; - /// Create new instance of [BufWriter] from [ToString]. pub fn new(content: impl ToString) -> Self { Self { buf: content.to_string() } @@ -64,6 +71,11 @@ impl BufWriter { writeln!(self.buf, "{}", Markdown::H3(heading)) } + /// Writes text in italics to the buffer formatted as [Markdown::Italic]. + pub fn write_italic(&mut self, text: &str) -> fmt::Result { + writeln!(self.buf, "{}", Markdown::Italic(text)) + } + /// Writes bold text to the bufffer formatted as [Markdown::Bold]. pub fn write_bold(&mut self, text: &str) -> fmt::Result { writeln!(self.buf, "{}", Markdown::Bold(text)) @@ -83,7 +95,7 @@ impl BufWriter { /// Writes a solidity code block block to the buffer. pub fn write_code(&mut self, code: &str) -> fmt::Result { - writeln!(self.buf, "{}", Markdown::CodeBlock("solidity", &code)) + writeln!(self.buf, "{}", Markdown::CodeBlock(SOLIDITY, &code)) } /// Write an item section to the buffer. First write comments, the item itself as code. @@ -117,11 +129,8 @@ impl BufWriter { self.write_bold(heading)?; self.writeln()?; - self.write_piped(&Self::PARAM_TABLE_HEADERS.join("|"))?; - - // TODO: lazy? - let separator = Self::PARAM_TABLE_HEADERS.iter().map(|h| "-".repeat(h.len())).join("|"); - self.write_piped(&separator)?; + self.write_piped(&PARAM_TABLE_HEADERS.join("|"))?; + self.write_piped(&PARAM_TABLE_SEPARATOR)?; for (index, param) in params.into_iter().enumerate() { let param_name = param.name.as_ref().map(|n| n.name.to_owned()); From 9a5a0bfed788c2d5077f5735722a450caf6a1676 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Wed, 11 Jan 2023 13:32:12 +0200 Subject: [PATCH 49/67] support user defined book.toml --- Cargo.lock | 1 + cli/src/cmd/forge/doc.rs | 11 +++--- config/src/doc.rs | 4 ++- doc/Cargo.toml | 1 + doc/src/builder.rs | 73 ++++++++++++++++++++++++++-------------- doc/src/helpers.rs | 28 +++++++++++++++ doc/src/lib.rs | 1 + 7 files changed, 89 insertions(+), 30 deletions(-) create mode 100644 doc/src/helpers.rs diff --git a/Cargo.lock b/Cargo.lock index e2f6047260ae..47db8c68d544 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2386,6 +2386,7 @@ dependencies = [ "eyre", "forge-fmt", "foundry-common", + "foundry-config", "futures-util", "itertools", "mdbook", diff --git a/cli/src/cmd/forge/doc.rs b/cli/src/cmd/forge/doc.rs index f154df7334ab..60520647182c 100644 --- a/cli/src/cmd/forge/doc.rs +++ b/cli/src/cmd/forge/doc.rs @@ -35,18 +35,21 @@ impl Cmd for DocArgs { fn run(self) -> eyre::Result { let root = self.root.clone().unwrap_or(find_project_root_path()?); let config = load_config_with_root(self.root.clone()); - let out = self.out.clone().unwrap_or(config.doc.out.clone()); + + let mut doc_config = config.doc.clone(); + if let Some(out) = self.out { + doc_config.out = out; + } DocBuilder::new(root, config.project_paths().sources) - .with_out(out.clone()) - .with_title(config.doc.title.clone()) + .with_config(doc_config.clone()) .with_fmt(config.fmt.clone()) .with_preprocessor(ContractInheritance) .with_preprocessor(Inheritdoc) .build()?; if self.serve { - Server::new(out).serve()?; + Server::new(doc_config.out).serve()?; } Ok(()) diff --git a/config/src/doc.rs b/config/src/doc.rs index 0bd8dcddafe7..2ca1c3d56e8e 100644 --- a/config/src/doc.rs +++ b/config/src/doc.rs @@ -11,10 +11,12 @@ pub struct DocConfig { pub out: PathBuf, /// The documentation title. pub title: String, + /// Path to user provided `book.toml`. + pub book: PathBuf, } impl Default for DocConfig { fn default() -> Self { - Self { out: PathBuf::from("docs"), title: "".to_owned() } + Self { out: PathBuf::from("docs"), title: String::default(), book: PathBuf::default() } } } diff --git a/doc/Cargo.toml b/doc/Cargo.toml index 7369f7a51853..74523ba4fefb 100644 --- a/doc/Cargo.toml +++ b/doc/Cargo.toml @@ -12,6 +12,7 @@ readme = "README.md" # foundry internal foundry-common = { path = "../common" } forge-fmt = { path = "../fmt" } +foundry-config = { path = "../config" } # ethers ethers-solc = { git = "https://github.com/gakonst/ethers-rs", default-features = false, features = ["async"] } diff --git a/doc/src/builder.rs b/doc/src/builder.rs index a1b29c93635d..f2e62046ea5e 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -1,5 +1,6 @@ use ethers_solc::utils::source_files_iter; use forge_fmt::{FormatterConfig, Visitable}; +use foundry_config::DocConfig; use itertools::Itertools; use rayon::prelude::*; use std::{ @@ -8,10 +9,11 @@ use std::{ fs, path::{Path, PathBuf}, }; +use toml::value; use crate::{ - document::DocumentContent, AsDoc, BufWriter, Document, ParseItem, ParseSource, Parser, - Preprocessor, + document::DocumentContent, helpers::merge_toml_table, AsDoc, BufWriter, Document, ParseItem, + ParseSource, Parser, Preprocessor, }; /// Build Solidity documentation for a project from natspec comments. @@ -23,10 +25,8 @@ pub struct DocBuilder { pub root: PathBuf, /// Path to Solidity source files. pub sources: PathBuf, - /// Output path. - pub out: PathBuf, - /// The documentation title. - pub title: String, + /// Documentation configuration. + pub config: DocConfig, /// The array of preprocessors to apply. pub preprocessors: Vec>, /// The formatter config. @@ -45,22 +45,15 @@ impl DocBuilder { Self { root, sources, - out: Default::default(), - title: Default::default(), + config: DocConfig::default(), preprocessors: Default::default(), fmt: Default::default(), } } - /// Set an `out` path on the builder. - pub fn with_out(mut self, out: PathBuf) -> Self { - self.out = out; - self - } - - /// Set title on the builder. - pub fn with_title(mut self, title: String) -> Self { - self.title = title; + /// Set config on the builder. + pub fn with_config(mut self, config: DocConfig) -> Self { + self.config = config; self } @@ -78,7 +71,7 @@ impl DocBuilder { /// Get the output directory pub fn out_dir(&self) -> PathBuf { - self.root.join(&self.out) + self.root.join(&self.config.out) } /// Parse the sources and build the documentation. @@ -137,7 +130,7 @@ impl DocBuilder { .into_iter() .map(|item| { let relative_path = path.strip_prefix(&self.root)?.join(item.filename()); - let target_path = self.out.join(Self::SRC).join(relative_path); + let target_path = self.config.out.join(Self::SRC).join(relative_path); let ident = item.source.ident(); Ok(Document::new(path.clone(), target_path) .with_content(DocumentContent::Single(item), ident)) @@ -157,7 +150,7 @@ impl DocBuilder { name }; let relative_path = path.strip_prefix(&self.root)?.join(filename); - let target_path = self.out.join(Self::SRC).join(relative_path); + let target_path = self.config.out.join(Self::SRC).join(relative_path); let identity = match filestem { Some(stem) if stem.to_lowercase().contains("constants") => stem.to_owned(), @@ -176,7 +169,7 @@ impl DocBuilder { for (ident, funcs) in overloaded { let filename = funcs.first().expect("no overloaded functions").filename(); let relative_path = path.strip_prefix(&self.root)?.join(filename); - let target_path = self.out.join(Self::SRC).join(relative_path); + let target_path = self.config.out.join(Self::SRC).join(relative_path); files.push( Document::new(path.clone(), target_path) .with_content(DocumentContent::OverloadedFunctions(funcs), ident), @@ -241,10 +234,12 @@ impl DocBuilder { fs::write(out_dir.join("book.css"), include_str!("../static/book.css"))?; // Write book config - let mut book: toml::Value = toml::from_str(include_str!("../static/book.toml"))?; - let book_entry = book["book"].as_table_mut().unwrap(); - book_entry.insert(String::from("title"), self.title.clone().into()); - fs::write(self.out_dir().join("book.toml"), toml::to_string_pretty(&book)?)?; + // TODO: + // let mut book: toml::value::Table = toml::from_str(include_str!("../static/book.toml"))?; + // let book_entry = book["book"].as_table_mut().unwrap(); + // book_entry.insert(String::from("title"), self.config.title.clone().into()); + // let book_config = self.book_config()?; // TODO: + fs::write(self.out_dir().join("book.toml"), self.book_config()?)?; // Write .gitignore let gitignore = "book/"; @@ -264,6 +259,34 @@ impl DocBuilder { Ok(()) } + fn book_config(&self) -> eyre::Result { + // Read the default book first + let mut book: value::Table = toml::from_str(include_str!("../static/book.toml"))?; + let book_entry = book["book"].as_table_mut().unwrap(); + book_entry.insert(String::from("title"), self.config.title.clone().into()); + + // Attempt to find the user provided book path + let book_path = { + if self.config.book.is_file() { + Some(self.config.book.clone()) + } else { + let book_path = self.config.book.join("book.toml"); + if book_path.is_file() { + Some(book_path) + } else { + None + } + } + }; + + // Merge two book configs + if let Some(book_path) = book_path { + merge_toml_table(&mut book, toml::from_str(&fs::read_to_string(book_path)?)?); + } + + Ok(toml::to_string_pretty(&book)?) + } + fn write_summary_section( &self, summary: &mut BufWriter, diff --git a/doc/src/helpers.rs b/doc/src/helpers.rs new file mode 100644 index 000000000000..0ae730e6f02c --- /dev/null +++ b/doc/src/helpers.rs @@ -0,0 +1,28 @@ +use toml::{value::Table, Value}; + +/// Merge original toml table with the override. +pub(crate) fn merge_toml_table(table: &mut Table, override_table: Table) { + for (key, override_value) in override_table { + match table.get_mut(&key) { + Some(Value::Table(inner_table)) => { + // Override value must be a table, otherwise discard + if let Value::Table(inner_override) = override_value { + merge_toml_table(inner_table, inner_override); + } + } + Some(Value::Array(inner_array)) => { + // Override value must be an arry, otherwise discard + if let Value::Array(inner_override) = override_value { + for entry in inner_override { + if !inner_array.contains(&entry) { + inner_array.push(entry); + } + } + } + } + _ => { + table.insert(key, override_value); + } + }; + } +} diff --git a/doc/src/lib.rs b/doc/src/lib.rs index 284b991e0105..a3025f5479ee 100644 --- a/doc/src/lib.rs +++ b/doc/src/lib.rs @@ -11,6 +11,7 @@ mod builder; mod document; +mod helpers; mod parser; mod preprocessor; mod server; From a9ffd2b4054c6093e003b3d1206de9a047747439 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Wed, 11 Jan 2023 15:24:53 +0200 Subject: [PATCH 50/67] add git source preprocessor --- cli/src/cmd/forge/doc.rs | 41 ++++++++++++++++++--- cli/src/opts/dependency.rs | 3 +- config/src/doc.rs | 10 +++++- doc/src/builder.rs | 12 +++++-- doc/src/preprocessor/git_source.rs | 42 ++++++++++++++++++++++ doc/src/preprocessor/inheritdoc.rs | 2 ++ doc/src/preprocessor/mod.rs | 6 ++++ doc/src/writer/as_doc.rs | 58 +++++++++++++----------------- doc/src/writer/writer.rs | 5 +++ 9 files changed, 137 insertions(+), 42 deletions(-) create mode 100644 doc/src/preprocessor/git_source.rs diff --git a/cli/src/cmd/forge/doc.rs b/cli/src/cmd/forge/doc.rs index 60520647182c..10230e6eee0d 100644 --- a/cli/src/cmd/forge/doc.rs +++ b/cli/src/cmd/forge/doc.rs @@ -1,8 +1,8 @@ -use crate::cmd::Cmd; +use crate::{cmd::Cmd, opts::GH_REPO_PREFIX_REGEX}; use clap::{Parser, ValueHint}; -use forge_doc::{ContractInheritance, DocBuilder, Inheritdoc, Server}; +use forge_doc::{ContractInheritance, DocBuilder, GitSource, Inheritdoc, Server}; use foundry_config::{find_project_root_path, load_config_with_root}; -use std::path::PathBuf; +use std::{path::PathBuf, process::Command}; #[derive(Debug, Clone, Parser)] pub struct DocArgs { @@ -34,18 +34,49 @@ impl Cmd for DocArgs { fn run(self) -> eyre::Result { let root = self.root.clone().unwrap_or(find_project_root_path()?); - let config = load_config_with_root(self.root.clone()); + let config = load_config_with_root(Some(root.clone())); let mut doc_config = config.doc.clone(); if let Some(out) = self.out { doc_config.out = out; } + if doc_config.repository.is_none() { + // Attempt to read repo from git + if let Ok(output) = Command::new("git").args(["remote", "get-url", "origin"]).output() { + if !output.stdout.is_empty() { + let remote = String::from_utf8(output.stdout)?.trim().to_owned(); + if let Some(captures) = GH_REPO_PREFIX_REGEX.captures(&remote) { + let brand = captures.name("brand").unwrap().as_str(); + let tld = captures.name("tld").unwrap().as_str(); + let project = GH_REPO_PREFIX_REGEX.replace(&remote, ""); + doc_config.repository = Some(format!( + "https://{brand}.{tld}/{}", + project.trim_end_matches(".git") + )); + } + } + } + } + + let commit = + Command::new("git").args(["rev-parse", "HEAD"]).output().ok().and_then(|output| { + if !output.stdout.is_empty() { + String::from_utf8(output.stdout).ok().map(|commit| commit.trim().to_owned()) + } else { + None + } + }); - DocBuilder::new(root, config.project_paths().sources) + DocBuilder::new(root.clone(), config.project_paths().sources) .with_config(doc_config.clone()) .with_fmt(config.fmt.clone()) .with_preprocessor(ContractInheritance) .with_preprocessor(Inheritdoc) + .with_preprocessor(GitSource { + root, + commit, + repository: doc_config.repository.clone(), + }) .build()?; if self.serve { diff --git a/cli/src/opts/dependency.rs b/cli/src/opts/dependency.rs index 374615b84fdf..869c2accebbb 100644 --- a/cli/src/opts/dependency.rs +++ b/cli/src/opts/dependency.rs @@ -7,7 +7,8 @@ use std::str::FromStr; static GH_REPO_REGEX: Lazy = Lazy::new(|| Regex::new("[A-Za-z\\d-]+/[A-Za-z\\d_.-]+").unwrap()); -static GH_REPO_PREFIX_REGEX: Lazy = Lazy::new(|| { +/// Git repo prefix regex +pub static GH_REPO_PREFIX_REGEX: Lazy = Lazy::new(|| { Regex::new(r"((git@)|(git\+https://)|(https://)|(org-([A-Za-z0-9-])+@))?(?P[A-Za-z0-9-]+)\.(?P[A-Za-z0-9-]+)(/|:)") .unwrap() }); diff --git a/config/src/doc.rs b/config/src/doc.rs index 2ca1c3d56e8e..4985e06f3da5 100644 --- a/config/src/doc.rs +++ b/config/src/doc.rs @@ -13,10 +13,18 @@ pub struct DocConfig { pub title: String, /// Path to user provided `book.toml`. pub book: PathBuf, + /// The repository url. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub repository: Option, } impl Default for DocConfig { fn default() -> Self { - Self { out: PathBuf::from("docs"), title: String::default(), book: PathBuf::default() } + Self { + out: PathBuf::from("docs"), + title: String::default(), + book: PathBuf::default(), + repository: None, + } } } diff --git a/doc/src/builder.rs b/doc/src/builder.rs index f2e62046ea5e..1233e45e2e35 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -262,8 +262,16 @@ impl DocBuilder { fn book_config(&self) -> eyre::Result { // Read the default book first let mut book: value::Table = toml::from_str(include_str!("../static/book.toml"))?; - let book_entry = book["book"].as_table_mut().unwrap(); - book_entry.insert(String::from("title"), self.config.title.clone().into()); + book["book"] + .as_table_mut() + .unwrap() + .insert(String::from("title"), self.config.title.clone().into()); + if let Some(ref repo) = self.config.repository { + book["output"].as_table_mut().unwrap()["html"] + .as_table_mut() + .unwrap() + .insert(String::from("git-repository-url"), repo.clone().into()); + } // Attempt to find the user provided book path let book_path = { diff --git a/doc/src/preprocessor/git_source.rs b/doc/src/preprocessor/git_source.rs new file mode 100644 index 000000000000..00d10364492e --- /dev/null +++ b/doc/src/preprocessor/git_source.rs @@ -0,0 +1,42 @@ +use std::path::PathBuf; + +use super::{Preprocessor, PreprocessorId}; +use crate::{Document, PreprocessorOutput}; + +/// [ContractInheritance] preprocessor id. +pub const GIT_SOURCE_ID: PreprocessorId = PreprocessorId("git_source"); + +/// The git source preprocessor. +/// +/// This preprocessor writes to [Document]'s context. +#[derive(Debug)] +pub struct GitSource { + /// The project root. + pub root: PathBuf, + /// The current commit hash. + pub commit: Option, + /// The repository url. + pub repository: Option, +} + +impl Preprocessor for GitSource { + fn id(&self) -> PreprocessorId { + GIT_SOURCE_ID + } + + fn preprocess(&self, documents: Vec) -> Result, eyre::Error> { + if let Some(ref repo) = self.repository { + let repo = repo.trim_end_matches("/"); + let commit = self.commit.clone().unwrap_or("master".to_owned()); + for document in documents.iter() { + let git_url = format!( + "{repo}/blob/{commit}/{}", + document.item_path.strip_prefix(&self.root)?.display() + ); + document.add_context(self.id(), PreprocessorOutput::GitSource(git_url)); + } + } + + Ok(documents) + } +} diff --git a/doc/src/preprocessor/inheritdoc.rs b/doc/src/preprocessor/inheritdoc.rs index 540d084c6c8e..e3f6a46c1b77 100644 --- a/doc/src/preprocessor/inheritdoc.rs +++ b/doc/src/preprocessor/inheritdoc.rs @@ -9,6 +9,8 @@ use crate::{ pub const INHERITDOC_ID: PreprocessorId = PreprocessorId("inheritdoc"); /// The inheritdoc preprocessor. +/// Traverses the documents and attempts to find inherited +/// comments for inheritdoc comment tags. /// /// This preprocessor writes to [Document]'s context. #[derive(Debug)] diff --git a/doc/src/preprocessor/mod.rs b/doc/src/preprocessor/mod.rs index 0e1d262299ca..6e20d8e3d5f8 100644 --- a/doc/src/preprocessor/mod.rs +++ b/doc/src/preprocessor/mod.rs @@ -9,6 +9,9 @@ pub use contract_inheritance::{ContractInheritance, CONTRACT_INHERITANCE_ID}; mod inheritdoc; pub use inheritdoc::{Inheritdoc, INHERITDOC_ID}; +mod git_source; +pub use git_source::{GitSource, GIT_SOURCE_ID}; + /// The preprocessor id. #[derive(Debug, Eq, Hash, PartialEq)] pub struct PreprocessorId(&'static str); @@ -24,6 +27,9 @@ pub enum PreprocessorOutput { /// The inheritdoc output. /// The map of inherited item keys to their comments. Inheritdoc(HashMap), + /// The git source output. + /// The git url of the item path. + GitSource(String), } /// Trait for preprocessing and/or modifying existing documents diff --git a/doc/src/writer/as_doc.rs b/doc/src/writer/as_doc.rs index 153bf8f6bb1c..4c5f68ef4b63 100644 --- a/doc/src/writer/as_doc.rs +++ b/doc/src/writer/as_doc.rs @@ -5,7 +5,7 @@ use crate::{ parser::ParseSource, writer::BufWriter, AsString, CommentTag, Comments, CommentsRef, Document, Markdown, PreprocessorOutput, - CONTRACT_INHERITANCE_ID, INHERITDOC_ID, + CONTRACT_INHERITANCE_ID, GIT_SOURCE_ID, INHERITDOC_ID, }; use itertools::Itertools; use solang_parser::pt::Base; @@ -77,6 +77,10 @@ impl AsDoc for Document { DocumentContent::OverloadedFunctions(items) => { writer .write_title(&format!("function {}", items.first().unwrap().source.ident()))?; + if let Some(git_source) = read_context!(self, GIT_SOURCE_ID, GitSource) { + writer.write_link("Git Source", &git_source)?; + writer.writeln()?; + } for item in items.iter() { let func = item.as_function().unwrap(); @@ -96,6 +100,10 @@ impl AsDoc for Document { } DocumentContent::Constants(items) => { writer.write_title("Constants")?; + if let Some(git_source) = read_context!(self, GIT_SOURCE_ID, GitSource) { + writer.write_link("Git Source", &git_source)?; + writer.writeln()?; + } for item in items.iter() { let var = item.as_variable().unwrap(); @@ -104,10 +112,14 @@ impl AsDoc for Document { } } DocumentContent::Single(item) => { + writer.write_title(&item.source.ident())?; + if let Some(git_source) = read_context!(self, GIT_SOURCE_ID, GitSource) { + writer.write_link("Git Source", &git_source)?; + writer.writeln()?; + } + match &item.source { ParseSource::Contract(contract) => { - writer.write_title(&contract.name.name)?; - if !contract.base.is_empty() { writer.write_bold("Inherits:")?; @@ -246,38 +258,9 @@ impl AsDoc for Document { })?; } } - ParseSource::Variable(var) => { - writer.write_title(&var.name.name)?; - writer.write_section(&item.comments, &item.code)?; - } - ParseSource::Event(event) => { - writer.write_title(&event.name.name)?; - writer.write_section(&item.comments, &item.code)?; - } - ParseSource::Error(error) => { - writer.write_title(&error.name.name)?; - writer.write_section(&item.comments, &item.code)?; - } - ParseSource::Struct(structure) => { - writer.write_title(&structure.name.name)?; - writer.write_section(&item.comments, &item.code)?; - } - ParseSource::Enum(enumerable) => { - writer.write_title(&enumerable.name.name)?; - writer.write_section(&item.comments, &item.code)?; - } - ParseSource::Type(ty) => { - writer.write_title(&ty.name.name)?; - writer.write_section(&item.comments, &item.code)?; - } + ParseSource::Function(func) => { // TODO: cleanup - // Write function name - let func_name = - func.name.as_ref().map_or(func.ty.to_string(), |n| n.name.to_owned()); - writer.write_heading(&func_name)?; - writer.writeln()?; - // Write function docs writer.writeln_doc( item.comments.exclude_tags(&[CommentTag::Param, CommentTag::Return]), @@ -302,6 +285,15 @@ impl AsDoc for Document { writer.writeln()?; } + + ParseSource::Variable(_) | + ParseSource::Event(_) | + ParseSource::Error(_) | + ParseSource::Struct(_) | + ParseSource::Enum(_) | + ParseSource::Type(_) => { + writer.write_section(&item.comments, &item.code)?; + } } } DocumentContent::Empty => (), diff --git a/doc/src/writer/writer.rs b/doc/src/writer/writer.rs index 05aec70f3aa6..5d1f4721af61 100644 --- a/doc/src/writer/writer.rs +++ b/doc/src/writer/writer.rs @@ -81,6 +81,11 @@ impl BufWriter { writeln!(self.buf, "{}", Markdown::Bold(text)) } + /// Writes link to the buffer formatted as [Markdown::Link]. + pub fn write_link(&mut self, name: &str, path: &str) -> fmt::Result { + writeln!(self.buf, "{}", Markdown::Link(name, path)) + } + /// Writes a list item to the bufffer indented by specified depth. pub fn write_list_item(&mut self, item: &str, depth: usize) -> fmt::Result { let indent = " ".repeat(depth * 2); From 38b96b35c26f2cc7784ee21b3a64a6c38a4ac1f6 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Wed, 11 Jan 2023 15:39:03 +0200 Subject: [PATCH 51/67] cleanup --- doc/src/builder.rs | 5 ----- doc/src/server.rs | 3 --- doc/src/writer/as_doc.rs | 1 - 3 files changed, 9 deletions(-) diff --git a/doc/src/builder.rs b/doc/src/builder.rs index 1233e45e2e35..3776fa95049e 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -234,11 +234,6 @@ impl DocBuilder { fs::write(out_dir.join("book.css"), include_str!("../static/book.css"))?; // Write book config - // TODO: - // let mut book: toml::value::Table = toml::from_str(include_str!("../static/book.toml"))?; - // let book_entry = book["book"].as_table_mut().unwrap(); - // book_entry.insert(String::from("title"), self.config.title.clone().into()); - // let book_config = self.book_config()?; // TODO: fs::write(self.out_dir().join("book.toml"), self.book_config()?)?; // Write .gitignore diff --git a/doc/src/server.rs b/doc/src/server.rs index 079a36f5b0a3..0694ed9ba2c2 100644 --- a/doc/src/server.rs +++ b/doc/src/server.rs @@ -94,9 +94,7 @@ async fn serve( |ws: warp::ws::Ws, mut rx: broadcast::Receiver| { ws.on_upgrade(move |ws| async move { let (mut user_ws_tx, _user_ws_rx) = ws.split(); - // TODO: trace!("websocket got connection"); if let Ok(m) = rx.recv().await { - // TODO: trace!("notify of reload"); let _ = user_ws_tx.send(m).await; } }) @@ -111,7 +109,6 @@ async fn serve( std::panic::set_hook(Box::new(move |_panic_info| { // exit if serve panics - // TODO: error!("Unable to serve: {}", panic_info); std::process::exit(1); })); diff --git a/doc/src/writer/as_doc.rs b/doc/src/writer/as_doc.rs index 4c5f68ef4b63..a5419b7643ff 100644 --- a/doc/src/writer/as_doc.rs +++ b/doc/src/writer/as_doc.rs @@ -189,7 +189,6 @@ impl AsDoc for Document { // Write function docs writer.writeln_doc( - // TODO: think about multiple inheritdocs comments.exclude_tags(&[CommentTag::Param, CommentTag::Return]), )?; From aac9bbcaee213dd0f77d3621562f2d5bd561edca Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Wed, 11 Jan 2023 15:43:31 +0200 Subject: [PATCH 52/67] clippy --- doc/src/builder.rs | 4 ++-- doc/src/parser/comment.rs | 2 +- doc/src/parser/item.rs | 2 +- doc/src/preprocessor/contract_inheritance.rs | 2 +- doc/src/preprocessor/git_source.rs | 2 +- doc/src/server.rs | 3 +-- doc/src/writer/as_doc.rs | 2 +- doc/src/writer/{writer.rs => buf_writer.rs} | 10 +++++----- doc/src/writer/mod.rs | 4 ++-- 9 files changed, 15 insertions(+), 16 deletions(-) rename doc/src/writer/{writer.rs => buf_writer.rs} (94%) diff --git a/doc/src/builder.rs b/doc/src/builder.rs index 3776fa95049e..4b2abcd6d8ea 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -343,7 +343,7 @@ impl DocBuilder { let summary_path = file.target_path.strip_prefix("docs/src")?; summary.write_link_list_item( - &ident, + ident, &summary_path.display().to_string(), depth, )?; @@ -352,7 +352,7 @@ impl DocBuilder { .map(|path| summary_path.strip_prefix(path)) .transpose()? .unwrap_or(summary_path); - readme.write_link_list_item(&ident, &readme_path.display().to_string(), 0)?; + readme.write_link_list_item(ident, &readme_path.display().to_string(), 0)?; } } else { let name = path.iter().last().unwrap().to_string_lossy(); diff --git a/doc/src/parser/comment.rs b/doc/src/parser/comment.rs index 7f72c70cb5e9..cdafbd28de48 100644 --- a/doc/src/parser/comment.rs +++ b/doc/src/parser/comment.rs @@ -64,7 +64,7 @@ impl Comment { /// Split the comment at first word. /// Useful for [CommentTag::Param] and [CommentTag::Return] comments. - pub fn split_first_word<'a>(&'a self) -> Option<(&'a str, &'a str)> { + pub fn split_first_word(&self) -> Option<(&str, &str)> { self.value.trim_start().split_once(' ') } diff --git a/doc/src/parser/item.rs b/doc/src/parser/item.rs index a70c5e0e7402..42b8b24ee2e4 100644 --- a/doc/src/parser/item.rs +++ b/doc/src/parser/item.rs @@ -83,7 +83,7 @@ impl ParseItem { let mut code = String::new(); let mut fmt = Formatter::new( &mut code, - &source, + source, FmtComments::default(), InlineConfig::default(), config, diff --git a/doc/src/preprocessor/contract_inheritance.rs b/doc/src/preprocessor/contract_inheritance.rs index 7581b04d4371..8bf567a7319d 100644 --- a/doc/src/preprocessor/contract_inheritance.rs +++ b/doc/src/preprocessor/contract_inheritance.rs @@ -47,7 +47,7 @@ impl Preprocessor for ContractInheritance { } impl ContractInheritance { - fn try_link_base<'a>(&self, base: &str, documents: &Vec) -> Option { + fn try_link_base(&self, base: &str, documents: &Vec) -> Option { for candidate in documents { if let DocumentContent::Single(ref item) = candidate.content { if let ParseSource::Contract(ref contract) = item.source { diff --git a/doc/src/preprocessor/git_source.rs b/doc/src/preprocessor/git_source.rs index 00d10364492e..6f922ee5a89f 100644 --- a/doc/src/preprocessor/git_source.rs +++ b/doc/src/preprocessor/git_source.rs @@ -26,7 +26,7 @@ impl Preprocessor for GitSource { fn preprocess(&self, documents: Vec) -> Result, eyre::Error> { if let Some(ref repo) = self.repository { - let repo = repo.trim_end_matches("/"); + let repo = repo.trim_end_matches('/'); let commit = self.commit.clone().unwrap_or("master".to_owned()); for document in documents.iter() { let git_url = format!( diff --git a/doc/src/server.rs b/doc/src/server.rs index 0694ed9ba2c2..81aed6b1264f 100644 --- a/doc/src/server.rs +++ b/doc/src/server.rs @@ -62,9 +62,8 @@ impl Server { // A channel used to broadcast to any websockets to reload when a file changes. let (tx, _rx) = tokio::sync::broadcast::channel::(100); - let reload_tx = tx.clone(); let thread_handle = std::thread::spawn(move || { - serve(build_dir, sockaddr, reload_tx, &file_404); + serve(build_dir, sockaddr, tx, &file_404); }); println!("Serving on: http://{address}"); diff --git a/doc/src/writer/as_doc.rs b/doc/src/writer/as_doc.rs index a5419b7643ff..97042516b3ed 100644 --- a/doc/src/writer/as_doc.rs +++ b/doc/src/writer/as_doc.rs @@ -193,7 +193,7 @@ impl AsDoc for Document { )?; // Write function header - writer.write_code(&code)?; + writer.write_code(code)?; // Write function parameter comments in a table let params = func diff --git a/doc/src/writer/writer.rs b/doc/src/writer/buf_writer.rs similarity index 94% rename from doc/src/writer/writer.rs rename to doc/src/writer/buf_writer.rs index 5d1f4721af61..071305a04853 100644 --- a/doc/src/writer/writer.rs +++ b/doc/src/writer/buf_writer.rs @@ -6,10 +6,10 @@ use std::fmt::{self, Display, Write}; use crate::{AsDoc, AsString, CommentTag, Comments, Markdown}; /// Solidity language name. -const SOLIDITY: &'static str = "solidity"; +const SOLIDITY: &str = "solidity"; /// Headers and separator for rendering parameter table. -const PARAM_TABLE_HEADERS: &'static [&'static str] = &["Name", "Type", "Description"]; +const PARAM_TABLE_HEADERS: &[&str] = &["Name", "Type", "Description"]; static PARAM_TABLE_SEPARATOR: Lazy = Lazy::new(|| PARAM_TABLE_HEADERS.iter().map(|h| "-".repeat(h.len())).join("|")); @@ -100,12 +100,12 @@ impl BufWriter { /// Writes a solidity code block block to the buffer. pub fn write_code(&mut self, code: &str) -> fmt::Result { - writeln!(self.buf, "{}", Markdown::CodeBlock(SOLIDITY, &code)) + writeln!(self.buf, "{}", Markdown::CodeBlock(SOLIDITY, code)) } /// Write an item section to the buffer. First write comments, the item itself as code. pub fn write_section(&mut self, comments: &Comments, code: &str) -> fmt::Result { - self.writeln_raw(&comments.as_doc()?)?; + self.writeln_raw(comments.as_doc()?)?; self.write_code(code)?; self.writeln() } @@ -137,7 +137,7 @@ impl BufWriter { self.write_piped(&PARAM_TABLE_HEADERS.join("|"))?; self.write_piped(&PARAM_TABLE_SEPARATOR)?; - for (index, param) in params.into_iter().enumerate() { + for (index, param) in params.iter().enumerate() { let param_name = param.name.as_ref().map(|n| n.name.to_owned()); let mut comment = param_name.as_ref().and_then(|name| { diff --git a/doc/src/writer/mod.rs b/doc/src/writer/mod.rs index d4d9f6ab7c09..3f7d9069ffad 100644 --- a/doc/src/writer/mod.rs +++ b/doc/src/writer/mod.rs @@ -2,10 +2,10 @@ mod as_doc; mod as_string; +mod buf_writer; mod markdown; -mod writer; pub use as_doc::{AsDoc, AsDocResult}; pub use as_string::AsString; +pub use buf_writer::BufWriter; pub use markdown::Markdown; -pub use writer::BufWriter; From d560b37d7776309203c35ad320abf1b51905818b Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Wed, 11 Jan 2023 16:00:34 +0200 Subject: [PATCH 53/67] add high level architecture of doc module --- doc/README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/doc/README.md b/doc/README.md index 805dce50d301..9e9c638aadd9 100644 --- a/doc/README.md +++ b/doc/README.md @@ -1 +1,27 @@ # Documentation (`doc`) + +Solidity documentation generator. It parses the source code and generates an mdbook +based on the parse tree and [NatSpec comments](https://docs.soliditylang.org/en/v0.8.17/natspec-format.html). + +## Architecture + +The entrypoint for the documentation module is the `DocBuilder`. +The `DocBuilder` generates the mdbook in 3 phases: + +1. Parse + +In this phase, builder invokes 2 parsers: [solang parser](https://github.com/hyperledger-labs/solang) and internal `Parser`. The solang parser produces the parse tree based on the source code. Afterwards, the internal parser walks the parse tree by implementing the `Visitor` trait from the `fmt` crate and saves important information about the parsed nodes, doc comments. + +Then, builder takes the output of the internal `Parser` and creates documents with additional information: the path of the original item, display identity, the target path where this document will be written. + + +2. Preprocess + +The builder accepts an array of preprocessors which can be applied to documents produced in the `Parse` phase. The preprocessors can rearrange and/or change the array as well as modify the separate documents. + +At the end of this phase, the builder maintains a possibly modified collection of documents. + + +3. Write + +At this point, builder has all necessary information to generate documentation for the source code. It takes every document, formats the source file contents and writes/copies additional files that are required for building documentation. \ No newline at end of file From 5d769fab5333c29e6acbf90e6dc948c9b04928d8 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Wed, 11 Jan 2023 16:01:12 +0200 Subject: [PATCH 54/67] clippy --- cli/src/cmd/forge/doc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/cmd/forge/doc.rs b/cli/src/cmd/forge/doc.rs index 10230e6eee0d..814f276f1824 100644 --- a/cli/src/cmd/forge/doc.rs +++ b/cli/src/cmd/forge/doc.rs @@ -69,7 +69,7 @@ impl Cmd for DocArgs { DocBuilder::new(root.clone(), config.project_paths().sources) .with_config(doc_config.clone()) - .with_fmt(config.fmt.clone()) + .with_fmt(config.fmt) .with_preprocessor(ContractInheritance) .with_preprocessor(Inheritdoc) .with_preprocessor(GitSource { From 6cb53dbe4435dd7b9fbfd3c5f2f3e58e7505eefd Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Wed, 11 Jan 2023 16:12:43 +0200 Subject: [PATCH 55/67] disable mdbook default features --- Cargo.lock | 132 +------------------------------------------------ doc/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 132 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 729bcfdba74f..298e3e35b9a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,19 +81,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "ammonia" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e6d1c7838db705c9b756557ee27c384ce695a1c51a6fe528784cb1c6840170" -dependencies = [ - "html5ever", - "maplit", - "once_cell", - "tendril", - "url", -] - [[package]] name = "android_system_properties" version = "0.1.5" @@ -1699,18 +1686,6 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" -[[package]] -name = "elasticlunr-rs" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94d9c8df0fe6879ca12e7633fdfe467c503722cc981fc463703472d2b876448" -dependencies = [ - "regex", - "serde", - "serde_derive", - "serde_json", -] - [[package]] name = "elliptic-curve" version = "0.12.3" @@ -2697,16 +2672,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" -[[package]] -name = "futf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" -dependencies = [ - "mac", - "new_debug_unreachable", -] - [[package]] name = "futures" version = "0.3.25" @@ -3083,15 +3048,6 @@ dependencies = [ "url", ] -[[package]] -name = "gitignore" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78aa90e4620c1498ac434c06ba6e521b525794bbdacf085d490cc794b4a2f9a4" -dependencies = [ - "glob", -] - [[package]] name = "glob" version = "0.3.1" @@ -3302,20 +3258,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "html5ever" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" -dependencies = [ - "log", - "mac", - "markup5ever", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "http" version = "0.2.8" @@ -3858,32 +3800,12 @@ dependencies = [ "cfg-if 1.0.0", ] -[[package]] -name = "mac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" - [[package]] name = "maplit" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" -[[package]] -name = "markup5ever" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" -dependencies = [ - "log", - "phf 0.10.1", - "phf_codegen 0.10.0", - "string_cache", - "string_cache_codegen", - "tendril", -] - [[package]] name = "matchers" version = "0.1.0" @@ -3931,20 +3853,14 @@ version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1ed28d5903dde77bd5182645078a37ee57014cac6ccb2d54e1d6496386648e4" dependencies = [ - "ammonia", "anyhow", "chrono", "clap 4.0.32", "clap_complete", - "elasticlunr-rs", "env_logger", - "futures-util", - "gitignore", "handlebars", "log", "memchr", - "notify", - "notify-debouncer-mini", "once_cell", "opener", "pulldown-cmark", @@ -3953,10 +3869,8 @@ dependencies = [ "serde_json", "shlex", "tempfile", - "tokio", "toml", "topological-sort", - "warp", ] [[package]] @@ -4195,16 +4109,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "notify-debouncer-mini" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e23e9fa24f094b143c1eb61f90ac6457de87be6987bc70746e0179f7dbc9007b" -dependencies = [ - "crossbeam-channel", - "notify", -] - [[package]] name = "ntapi" version = "0.3.7" @@ -4760,16 +4664,6 @@ dependencies = [ "phf_shared 0.8.0", ] -[[package]] -name = "phf_codegen" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" -dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", -] - [[package]] name = "phf_generator" version = "0.8.0" @@ -6149,19 +6043,6 @@ dependencies = [ "parking_lot 0.12.1", "phf_shared 0.10.0", "precomputed-hash", - "serde", -] - -[[package]] -name = "string_cache_codegen" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" -dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", - "proc-macro2", - "quote", ] [[package]] @@ -6304,17 +6185,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "tendril" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" -dependencies = [ - "futf", - "mac", - "utf-8", -] - [[package]] name = "term" version = "0.7.0" @@ -6365,7 +6235,7 @@ dependencies = [ "fnv", "nom 5.1.2", "phf 0.8.0", - "phf_codegen 0.8.0", + "phf_codegen", ] [[package]] diff --git a/doc/Cargo.toml b/doc/Cargo.toml index 74523ba4fefb..3d6c44e71a4e 100644 --- a/doc/Cargo.toml +++ b/doc/Cargo.toml @@ -27,7 +27,7 @@ clap = { version = "3.0.10", features = [ ] } # mdbook -mdbook = "0.4.25" +mdbook = { version = "0.4.25", default-features = false } warp = { version = "0.3.2", default-features = false, features = ["websocket"] } tokio = { version = "1", features = ["macros", "rt-multi-thread"] } futures-util = "0.3.4" From e5f56c2358c591ef01d0244a075ab3cb31ea18d4 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Wed, 11 Jan 2023 18:04:58 +0200 Subject: [PATCH 56/67] export hostname and port as cli options --- cli/src/cmd/forge/doc.rs | 11 ++++++++++- doc/src/server.rs | 16 ++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/cli/src/cmd/forge/doc.rs b/cli/src/cmd/forge/doc.rs index 814f276f1824..70a57ab3ef3a 100644 --- a/cli/src/cmd/forge/doc.rs +++ b/cli/src/cmd/forge/doc.rs @@ -27,6 +27,12 @@ pub struct DocArgs { #[clap(help = "Serve the documentation.", long, short)] serve: bool, + + #[clap(help = "Hostname for serving documentation.", long, requires = "serve")] + hostname: Option, + + #[clap(help = "Port for serving documentation.", long, short, requires = "serve")] + port: Option, } impl Cmd for DocArgs { @@ -80,7 +86,10 @@ impl Cmd for DocArgs { .build()?; if self.serve { - Server::new(doc_config.out).serve()?; + Server::new(doc_config.out) + .with_hostname(self.hostname.unwrap_or("localhost".to_owned())) + .with_port(self.port.unwrap_or(3000)) + .serve()?; } Ok(()) diff --git a/doc/src/server.rs b/doc/src/server.rs index 81aed6b1264f..970cff41ecde 100644 --- a/doc/src/server.rs +++ b/doc/src/server.rs @@ -15,12 +15,12 @@ const LIVE_RELOAD_ENDPOINT: &str = "__livereload"; pub struct Server { path: PathBuf, hostname: String, - port: String, + port: usize, } impl Default for Server { fn default() -> Self { - Self { path: PathBuf::default(), hostname: "localhost".to_owned(), port: "3000".to_owned() } + Self { path: PathBuf::default(), hostname: "localhost".to_owned(), port: 3000 } } } @@ -30,6 +30,18 @@ impl Server { Self { path, ..Default::default() } } + /// Set host on the [Server]. + pub fn with_hostname(mut self, hostname: String) -> Self { + self.hostname = hostname; + self + } + + /// Set port on the [Server]. + pub fn with_port(mut self, port: usize) -> Self { + self.port = port; + self + } + /// Serve the mdbook. pub fn serve(self) -> eyre::Result<()> { let mut book = From 3ec884fe2fd4b00c3d34ea52b2bafabce7e503e7 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Wed, 11 Jan 2023 18:47:31 +0200 Subject: [PATCH 57/67] fix summary path prefix stripping --- doc/src/builder.rs | 4 +++- doc/src/parser/mod.rs | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/doc/src/builder.rs b/doc/src/builder.rs index 4b2abcd6d8ea..7f157245b3bd 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -341,7 +341,9 @@ impl DocBuilder { for file in files { let ident = &file.identity; - let summary_path = file.target_path.strip_prefix("docs/src")?; + let summary_path = file + .target_path + .strip_prefix(self.out_dir().strip_prefix(&self.root)?.join(Self::SRC))?; summary.write_link_list_item( ident, &summary_path.display().to_string(), diff --git a/doc/src/parser/mod.rs b/doc/src/parser/mod.rs index 7eb15c694e93..f10243b16983 100644 --- a/doc/src/parser/mod.rs +++ b/doc/src/parser/mod.rs @@ -292,5 +292,28 @@ mod tests { assert!(contract.children.iter().all(|ch| ch.comments.is_empty())); } + #[test] + fn contract_with_fallback() { + let items = parse_source( + r#" + contract Contract { + fallback() external payable {} + } + "#, + ); + + assert_eq!(items.len(), 1); + + let contract = items.first().unwrap(); + assert!(contract.comments.is_empty()); + assert_eq!(contract.children.len(), 1); + assert_eq!(contract.source.ident(), "Contract"); + assert_matches!(contract.source, ParseSource::Contract(_)); + + let fallback = contract.children.first().unwrap(); + assert_eq!(fallback.source.ident(), "fallback"); + assert_matches!(fallback.source, ParseSource::Function(_)); + } + // TODO: test regular doc comments & natspec } From f5ac35ce59076855bc9772a0df40a7687659fb94 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Wed, 11 Jan 2023 09:31:07 -0800 Subject: [PATCH 58/67] Apply suggestions from code review Co-authored-by: Matthias Seitz --- config/src/doc.rs | 1 - doc/src/builder.rs | 1 - doc/src/preprocessor/contract_inheritance.rs | 3 ++- doc/src/preprocessor/git_source.rs | 1 - 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/config/src/doc.rs b/config/src/doc.rs index 4985e06f3da5..5ea230859fd5 100644 --- a/config/src/doc.rs +++ b/config/src/doc.rs @@ -1,7 +1,6 @@ //! Configuration specific to the `forge doc` command and the `forge_doc` package use std::path::PathBuf; - use serde::{Deserialize, Serialize}; /// Contains the config for parsing and rendering docs diff --git a/doc/src/builder.rs b/doc/src/builder.rs index 7f157245b3bd..e0049aaea8ad 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -10,7 +10,6 @@ use std::{ path::{Path, PathBuf}, }; use toml::value; - use crate::{ document::DocumentContent, helpers::merge_toml_table, AsDoc, BufWriter, Document, ParseItem, ParseSource, Parser, Preprocessor, diff --git a/doc/src/preprocessor/contract_inheritance.rs b/doc/src/preprocessor/contract_inheritance.rs index 8bf567a7319d..51553a0812d0 100644 --- a/doc/src/preprocessor/contract_inheritance.rs +++ b/doc/src/preprocessor/contract_inheritance.rs @@ -11,7 +11,8 @@ pub const CONTRACT_INHERITANCE_ID: PreprocessorId = PreprocessorId("contract_inh /// to link them with the paths of the other contract documents. /// /// This preprocessor writes to [Document]'s context. -#[derive(Debug)] +#[derive(Default, Debug)] +#[non_exhaustive] pub struct ContractInheritance; impl Preprocessor for ContractInheritance { diff --git a/doc/src/preprocessor/git_source.rs b/doc/src/preprocessor/git_source.rs index 6f922ee5a89f..afcf6f935fd9 100644 --- a/doc/src/preprocessor/git_source.rs +++ b/doc/src/preprocessor/git_source.rs @@ -1,5 +1,4 @@ use std::path::PathBuf; - use super::{Preprocessor, PreprocessorId}; use crate::{Document, PreprocessorOutput}; From db55dda1cbcb290ca2cbf4624b9d51e2e2a98dfb Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Wed, 11 Jan 2023 19:32:15 +0200 Subject: [PATCH 59/67] fmt --- config/src/doc.rs | 2 +- doc/src/builder.rs | 8 ++++---- doc/src/helpers.rs | 2 +- doc/src/preprocessor/git_source.rs | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/config/src/doc.rs b/config/src/doc.rs index 5ea230859fd5..1596a01390ea 100644 --- a/config/src/doc.rs +++ b/config/src/doc.rs @@ -1,7 +1,7 @@ //! Configuration specific to the `forge doc` command and the `forge_doc` package -use std::path::PathBuf; use serde::{Deserialize, Serialize}; +use std::path::PathBuf; /// Contains the config for parsing and rendering docs #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] diff --git a/doc/src/builder.rs b/doc/src/builder.rs index e0049aaea8ad..0f2a2d4d94c9 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -1,3 +1,7 @@ +use crate::{ + document::DocumentContent, helpers::merge_toml_table, AsDoc, BufWriter, Document, ParseItem, + ParseSource, Parser, Preprocessor, +}; use ethers_solc::utils::source_files_iter; use forge_fmt::{FormatterConfig, Visitable}; use foundry_config::DocConfig; @@ -10,10 +14,6 @@ use std::{ path::{Path, PathBuf}, }; use toml::value; -use crate::{ - document::DocumentContent, helpers::merge_toml_table, AsDoc, BufWriter, Document, ParseItem, - ParseSource, Parser, Preprocessor, -}; /// Build Solidity documentation for a project from natspec comments. /// The builder parses the source files using [Parser], diff --git a/doc/src/helpers.rs b/doc/src/helpers.rs index 0ae730e6f02c..013d606c76e3 100644 --- a/doc/src/helpers.rs +++ b/doc/src/helpers.rs @@ -11,7 +11,7 @@ pub(crate) fn merge_toml_table(table: &mut Table, override_table: Table) { } } Some(Value::Array(inner_array)) => { - // Override value must be an arry, otherwise discard + // Override value must be an array, otherwise discard if let Value::Array(inner_override) = override_value { for entry in inner_override { if !inner_array.contains(&entry) { diff --git a/doc/src/preprocessor/git_source.rs b/doc/src/preprocessor/git_source.rs index afcf6f935fd9..4b338e4fdb65 100644 --- a/doc/src/preprocessor/git_source.rs +++ b/doc/src/preprocessor/git_source.rs @@ -1,6 +1,6 @@ -use std::path::PathBuf; use super::{Preprocessor, PreprocessorId}; use crate::{Document, PreprocessorOutput}; +use std::path::PathBuf; /// [ContractInheritance] preprocessor id. pub const GIT_SOURCE_ID: PreprocessorId = PreprocessorId("git_source"); From 9171ac1a6144e19d338f51c5bbb96f00f194d086 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Wed, 11 Jan 2023 19:36:47 +0200 Subject: [PATCH 60/67] fix non exhaustive structs --- cli/src/cmd/forge/doc.rs | 4 ++-- doc/src/preprocessor/inheritdoc.rs | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/cli/src/cmd/forge/doc.rs b/cli/src/cmd/forge/doc.rs index 70a57ab3ef3a..b762cc95411f 100644 --- a/cli/src/cmd/forge/doc.rs +++ b/cli/src/cmd/forge/doc.rs @@ -76,8 +76,8 @@ impl Cmd for DocArgs { DocBuilder::new(root.clone(), config.project_paths().sources) .with_config(doc_config.clone()) .with_fmt(config.fmt) - .with_preprocessor(ContractInheritance) - .with_preprocessor(Inheritdoc) + .with_preprocessor(ContractInheritance::default()) + .with_preprocessor(Inheritdoc::default()) .with_preprocessor(GitSource { root, commit, diff --git a/doc/src/preprocessor/inheritdoc.rs b/doc/src/preprocessor/inheritdoc.rs index e3f6a46c1b77..c411f53f9247 100644 --- a/doc/src/preprocessor/inheritdoc.rs +++ b/doc/src/preprocessor/inheritdoc.rs @@ -13,7 +13,8 @@ pub const INHERITDOC_ID: PreprocessorId = PreprocessorId("inheritdoc"); /// comments for inheritdoc comment tags. /// /// This preprocessor writes to [Document]'s context. -#[derive(Debug)] +#[derive(Default, Debug)] +#[non_exhaustive] pub struct Inheritdoc; impl Preprocessor for Inheritdoc { From 981a8020150b972ea38d5d4139c2f929611c9105 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Wed, 11 Jan 2023 20:50:12 +0200 Subject: [PATCH 61/67] filter out @solidity tags --- doc/src/builder.rs | 3 +++ doc/src/parser/mod.rs | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/doc/src/builder.rs b/doc/src/builder.rs index 0f2a2d4d94c9..46e9de5588b0 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -87,6 +87,7 @@ impl DocBuilder { .par_iter() .enumerate() .map(|(i, path)| { + // Read and parse source file let source = fs::read_to_string(path)?; let (mut source_unit, comments) = solang_parser::parse(&source, i).map_err(|diags| { @@ -96,6 +97,8 @@ impl DocBuilder { diags ) })?; + + // Visit the parse tree let mut doc = Parser::new(comments, source).with_fmt(self.fmt.clone()); source_unit .visit(&mut doc) diff --git a/doc/src/parser/mod.rs b/doc/src/parser/mod.rs index f10243b16983..b7d1875d39a6 100644 --- a/doc/src/parser/mod.rs +++ b/doc/src/parser/mod.rs @@ -1,6 +1,7 @@ //! The parser module. use forge_fmt::{FormatterConfig, Visitable, Visitor}; +use itertools::Itertools; use solang_parser::{ doccomment::{parse_doccomments, DocComment}, pt::{ @@ -111,6 +112,10 @@ impl Parser { DocComment::Block { comments } => res.extend(comments.into_iter()), } } + + // Filter out `@solidity` tags + // See https://docs.soliditylang.org/en/v0.8.17/assembly.html#memory-safety + let res = res.into_iter().filter(|c| c.tag.trim() != "solidity").collect_vec(); Ok(res.try_into()?) } } @@ -196,7 +201,7 @@ mod tests { #[inline] fn parse_source(src: &str) -> Vec { let (mut source, comments) = parse(src, 0).expect("failed to parse source"); - let mut doc = Parser::new(comments, src.to_owned()); // TODO: + let mut doc = Parser::new(comments, src.to_owned()); source.visit(&mut doc).expect("failed to visit source"); doc.items() } From 77a1fc3f30111f8c44a6255f73e63b373d1f7ecf Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Wed, 11 Jan 2023 22:02:31 +0200 Subject: [PATCH 62/67] add it test and build flag --- cli/src/cmd/forge/doc.rs | 4 ++++ cli/tests/it/doc.rs | 11 +++++++++++ cli/tests/it/main.rs | 2 ++ doc/src/builder.rs | 17 +++++++++++++++++ 4 files changed, 34 insertions(+) create mode 100644 cli/tests/it/doc.rs diff --git a/cli/src/cmd/forge/doc.rs b/cli/src/cmd/forge/doc.rs index b762cc95411f..02c54748da9a 100644 --- a/cli/src/cmd/forge/doc.rs +++ b/cli/src/cmd/forge/doc.rs @@ -25,6 +25,9 @@ pub struct DocArgs { )] out: Option, + #[clap(help = "Build the documentation.", long, short)] + build: bool, + #[clap(help = "Serve the documentation.", long, short)] serve: bool, @@ -74,6 +77,7 @@ impl Cmd for DocArgs { }); DocBuilder::new(root.clone(), config.project_paths().sources) + .with_should_build(self.build) .with_config(doc_config.clone()) .with_fmt(config.fmt) .with_preprocessor(ContractInheritance::default()) diff --git a/cli/tests/it/doc.rs b/cli/tests/it/doc.rs new file mode 100644 index 000000000000..82d38b147b09 --- /dev/null +++ b/cli/tests/it/doc.rs @@ -0,0 +1,11 @@ +use foundry_cli_test_utils::util::{setup_forge_remote, RemoteProject}; + +#[test] +fn can_generate_solmate_docs() { + let (prj, _) = + setup_forge_remote(RemoteProject::new("transmissions11/solmate").set_build(false)); + prj.forge_command() + .args(["doc", "--build"]) + .ensure_execute_success() + .expect("`forge doc` failed"); +} diff --git a/cli/tests/it/main.rs b/cli/tests/it/main.rs index 94f0048061db..6ae4c7a5e619 100644 --- a/cli/tests/it/main.rs +++ b/cli/tests/it/main.rs @@ -9,6 +9,8 @@ mod config; #[cfg(not(feature = "external-integration-tests"))] mod create; #[cfg(not(feature = "external-integration-tests"))] +mod doc; +#[cfg(not(feature = "external-integration-tests"))] mod multi_script; #[cfg(not(feature = "external-integration-tests"))] mod script; diff --git a/doc/src/builder.rs b/doc/src/builder.rs index 46e9de5588b0..18e94dee34d9 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -6,6 +6,7 @@ use ethers_solc::utils::source_files_iter; use forge_fmt::{FormatterConfig, Visitable}; use foundry_config::DocConfig; use itertools::Itertools; +use mdbook::MDBook; use rayon::prelude::*; use std::{ cmp::Ordering, @@ -24,6 +25,8 @@ pub struct DocBuilder { pub root: PathBuf, /// Path to Solidity source files. pub sources: PathBuf, + /// Flag whether to build mdbook. + pub should_build: bool, /// Documentation configuration. pub config: DocConfig, /// The array of preprocessors to apply. @@ -44,12 +47,19 @@ impl DocBuilder { Self { root, sources, + should_build: false, config: DocConfig::default(), preprocessors: Default::default(), fmt: Default::default(), } } + /// Set `shoul_build` flag on the builder + pub fn with_should_build(mut self, should_build: bool) -> Self { + self.should_build = should_build; + self + } + /// Set config on the builder. pub fn with_config(mut self, config: DocConfig) -> Self { self.config = config; @@ -199,6 +209,13 @@ impl DocBuilder { // Write mdbook related files self.write_mdbook(documents.collect_vec())?; + // Build the book if requested + if self.should_build { + MDBook::load(&self.out_dir()) + .and_then(|book| book.build()) + .map_err(|err| eyre::eyre!("failed to build book: {err:?}"))?; + } + Ok(()) } From e512047004c08f368e93058907488fd59ef56f96 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Wed, 11 Jan 2023 22:07:34 +0200 Subject: [PATCH 63/67] remove serve panic hook --- doc/src/server.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/doc/src/server.rs b/doc/src/server.rs index 970cff41ecde..bd72a354aea5 100644 --- a/doc/src/server.rs +++ b/doc/src/server.rs @@ -117,11 +117,5 @@ async fn serve( let fallback_route = warp::fs::file(build_dir.join(file_404)) .map(|reply| warp::reply::with_status(reply, warp::http::StatusCode::NOT_FOUND)); let routes = livereload.or(book_route).or(fallback_route); - - std::panic::set_hook(Box::new(move |_panic_info| { - // exit if serve panics - std::process::exit(1); - })); - warp::serve(routes).run(address).await; } From 7c135cd969092710ad53d2532c3113a9a4517344 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Wed, 11 Jan 2023 22:19:44 +0200 Subject: [PATCH 64/67] clippy --- doc/src/builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/builder.rs b/doc/src/builder.rs index 18e94dee34d9..ae940c0ea8c2 100644 --- a/doc/src/builder.rs +++ b/doc/src/builder.rs @@ -211,7 +211,7 @@ impl DocBuilder { // Build the book if requested if self.should_build { - MDBook::load(&self.out_dir()) + MDBook::load(self.out_dir()) .and_then(|book| book.build()) .map_err(|err| eyre::eyre!("failed to build book: {err:?}"))?; } From 5474ed680511a94a7cfc8137909e1bf59f9b33e4 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Mon, 16 Jan 2023 11:53:20 +0200 Subject: [PATCH 65/67] custom:name and custom:param support --- doc/src/parser/comment.rs | 8 +++++++- doc/src/parser/mod.rs | 38 +++++++++++++++++++++++++++++++----- doc/src/writer/buf_writer.rs | 8 +++----- 3 files changed, 43 insertions(+), 11 deletions(-) diff --git a/doc/src/parser/comment.rs b/doc/src/parser/comment.rs index cdafbd28de48..2e3d4bbb19b3 100644 --- a/doc/src/parser/comment.rs +++ b/doc/src/parser/comment.rs @@ -38,7 +38,13 @@ impl FromStr for CommentTag { "return" => CommentTag::Return, "inheritdoc" => CommentTag::Inheritdoc, _ if trimmed.starts_with("custom:") => { - CommentTag::Custom(trimmed.trim_start_matches("custom:").trim().to_owned()) + // `@custom:param` tag will be parsed as `CommentTag::Param` due to a limitation + // on specifying parameter docs for unnamed function arguments. + let custom_tag = trimmed.trim_start_matches("custom:").trim(); + match custom_tag { + "param" => CommentTag::Param, + _ => CommentTag::Custom(custom_tag.to_owned()), + } } _ => eyre::bail!("unknown comment tag: {trimmed}"), }; diff --git a/doc/src/parser/mod.rs b/doc/src/parser/mod.rs index b7d1875d39a6..19c9c948a0af 100644 --- a/doc/src/parser/mod.rs +++ b/doc/src/parser/mod.rs @@ -6,8 +6,8 @@ use solang_parser::{ doccomment::{parse_doccomments, DocComment}, pt::{ Comment as SolangComment, EnumDefinition, ErrorDefinition, EventDefinition, - FunctionDefinition, Loc, SourceUnit, SourceUnitPart, StructDefinition, TypeDefinition, - VariableDefinition, + FunctionDefinition, Identifier, Loc, SourceUnit, SourceUnitPart, StructDefinition, + TypeDefinition, VariableDefinition, }, }; @@ -105,17 +105,25 @@ impl Parser { /// Parse the doc comments from the current start location. fn parse_docs(&mut self, end: usize) -> ParserResult { + self.parse_docs_range(self.context.doc_start_loc, end) + } + + /// Parse doc comments from the within specified range. + fn parse_docs_range(&mut self, start: usize, end: usize) -> ParserResult { let mut res = vec![]; - for comment in parse_doccomments(&self.comments, self.context.doc_start_loc, end) { + for comment in parse_doccomments(&self.comments, start, end) { match comment { DocComment::Line { comment } => res.push(comment), DocComment::Block { comments } => res.extend(comments.into_iter()), } } - // Filter out `@solidity` tags + // Filter out `@solidity` and empty tags // See https://docs.soliditylang.org/en/v0.8.17/assembly.html#memory-safety - let res = res.into_iter().filter(|c| c.tag.trim() != "solidity").collect_vec(); + let res = res + .into_iter() + .filter(|c| c.tag.trim() != "solidity" && !c.tag.trim().is_empty()) + .collect_vec(); Ok(res.try_into()?) } } @@ -164,6 +172,26 @@ impl Visitor for Parser { } fn visit_function(&mut self, func: &mut FunctionDefinition) -> ParserResult<()> { + // If the function parameter doesn't have a name, try to set it with + // `@custom:name` tag if any was provided + let mut start_loc = func.loc.start(); + for (loc, param) in func.params.iter_mut() { + if let Some(param) = param { + if param.name.is_none() { + let docs = self.parse_docs_range(start_loc, loc.end())?; + let name_tag = + docs.iter().find(|c| c.tag == CommentTag::Custom("name".to_owned())); + if let Some(name_tag) = name_tag { + if let Some(name) = name_tag.value.trim().split(' ').next() { + param.name = + Some(Identifier { loc: Loc::Implicit, name: name.to_owned() }) + } + } + } + } + start_loc = loc.end(); + } + self.add_element_to_parent(ParseSource::Function(func.clone()), func.loc) } diff --git a/doc/src/writer/buf_writer.rs b/doc/src/writer/buf_writer.rs index 071305a04853..8db5ac2271a3 100644 --- a/doc/src/writer/buf_writer.rs +++ b/doc/src/writer/buf_writer.rs @@ -141,21 +141,19 @@ impl BufWriter { let param_name = param.name.as_ref().map(|n| n.name.to_owned()); let mut comment = param_name.as_ref().and_then(|name| { - comments.iter().find_map(|comment| { - comment.match_first_word(name.as_str()).map(|rest| rest.replace('\n', " ")) - }) + comments.iter().find_map(|comment| comment.match_first_word(name.as_str())) }); // If it's a return tag and couldn't match by first word, // lookup the doc by index. if comment.is_none() && matches!(tag, CommentTag::Return) { - comment = comments.get(index).map(|c| c.value.clone()); + comment = comments.get(index).map(|c| &*c.value); } let row = [ Markdown::Code(¶m_name.unwrap_or_else(|| "".to_owned())).as_doc()?, Markdown::Code(¶m.ty.as_string()).as_doc()?, - comment.unwrap_or_default(), + comment.unwrap_or_default().replace('\n', " "), ]; self.write_piped(&row.join("|"))?; } From 9de1b8c12dff4c9a85dbc78b002b3d10a961f979 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Mon, 16 Jan 2023 12:48:34 +0200 Subject: [PATCH 66/67] specify doc.book default --- config/src/doc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/src/doc.rs b/config/src/doc.rs index 1596a01390ea..88914bafa463 100644 --- a/config/src/doc.rs +++ b/config/src/doc.rs @@ -21,8 +21,8 @@ impl Default for DocConfig { fn default() -> Self { Self { out: PathBuf::from("docs"), + book: PathBuf::from("book.toml"), title: String::default(), - book: PathBuf::default(), repository: None, } } From 9bbd271e6212bcd69bf4c6bedd0d2f98e92c6970 Mon Sep 17 00:00:00 2001 From: Roman Krasiuk Date: Mon, 16 Jan 2023 13:24:02 +0200 Subject: [PATCH 67/67] amend docs --- cli/src/cmd/forge/doc.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/src/cmd/forge/doc.rs b/cli/src/cmd/forge/doc.rs index 02c54748da9a..df2f79144741 100644 --- a/cli/src/cmd/forge/doc.rs +++ b/cli/src/cmd/forge/doc.rs @@ -17,7 +17,7 @@ pub struct DocArgs { #[clap( help = "The doc's output path.", - long_help = "The path where the docs are gonna get generated. By default, this is gonna be the docs directory at the root of the project.", + long_help = "The output path for the generated mdbook. By default, it is the `docs/` in project root.", long = "out", short, value_hint = ValueHint::DirPath, @@ -25,7 +25,7 @@ pub struct DocArgs { )] out: Option, - #[clap(help = "Build the documentation.", long, short)] + #[clap(help = "Build the `mdbook` from generated files.", long, short)] build: bool, #[clap(help = "Serve the documentation.", long, short)]