Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion crates/chisel/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ path = "bin/main.rs"

[dependencies]
# forge
forge-doc.workspace = true
forge-fmt.workspace = true
foundry-cli.workspace = true
foundry-common.workspace = true
Expand Down
13 changes: 6 additions & 7 deletions crates/chisel/src/source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
//! execution helpers.

use eyre::Result;
use forge_doc::solang_ext::{CodeLocationExt, SafeUnwrap};
use foundry_common::fs;
use foundry_compilers::{
Artifact, ProjectCompileOutput,
Expand All @@ -17,7 +16,7 @@ use foundry_config::{Config, SolcReq};
use foundry_evm::{backend::Backend, core::bytecode::InstIter, opts::EvmOpts};
use semver::Version;
use serde::{Deserialize, Serialize};
use solang_parser::pt;
use solang_parser::pt::{self, CodeLocation};
use solar::interface::diagnostics::EmittedDiagnostics;
use std::{cell::OnceCell, collections::HashMap, fmt, path::PathBuf};
use walkdir::WalkDir;
Expand Down Expand Up @@ -789,20 +788,20 @@ contract {contract_name} {{
}
}
pt::ContractPart::EventDefinition(def) => {
let event_name = def.name.safe_unwrap().name.clone();
let event_name = def.name.as_ref().unwrap().name.clone();
intermediate.event_definitions.insert(event_name, def);
}
pt::ContractPart::StructDefinition(def) => {
let struct_name = def.name.safe_unwrap().name.clone();
let struct_name = def.name.as_ref().unwrap().name.clone();
intermediate.struct_definitions.insert(struct_name, def);
}
pt::ContractPart::VariableDefinition(def) => {
let var_name = def.name.safe_unwrap().name.clone();
let var_name = def.name.as_ref().unwrap().name.clone();
intermediate.variable_definitions.insert(var_name, def);
}
_ => {}
});
Some((cd.name.safe_unwrap().name.clone(), intermediate))
Some((cd.name.as_ref().unwrap().name.clone(), intermediate))
}
_ => None,
})
Expand All @@ -823,7 +822,7 @@ contract {contract_name} {{
pub fn get_statement_definitions(statement: &pt::Statement) -> Vec<(String, pt::Expression)> {
match statement {
pt::Statement::VariableDefinition(_, def, _) => {
vec![(def.name.safe_unwrap().name.clone(), def.ty.clone())]
vec![(def.name.as_ref().unwrap().name.clone(), def.ty.clone())]
}
pt::Statement::Expression(_, pt::Expression::Assign(_, left, _)) => {
if let pt::Expression::List(_, list) = left.as_ref() {
Expand Down
2 changes: 0 additions & 2 deletions crates/doc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ repository.workspace = true
workspace = true

[dependencies]
forge-fmt.workspace = true
foundry-common.workspace = true
foundry-compilers.workspace = true
foundry-config.workspace = true
Expand All @@ -29,7 +28,6 @@ mdbook-driver = { version = "0.5", default-features = false, features = ["search
rayon.workspace = true
serde_json.workspace = true
serde.workspace = true
solang-parser.workspace = true
thiserror.workspace = true
toml.workspace = true
tracing.workspace = true
Expand Down
40 changes: 13 additions & 27 deletions crates/doc/src/builder.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::{
AsDoc, BufWriter, Document, ParseItem, ParseSource, Parser, Preprocessor,
document::DocumentContent, helpers::merge_toml_table, solang_ext::Visitable,
document::DocumentContent, helpers::merge_toml_table,
};
use alloy_primitives::map::HashMap;
use eyre::{Context, Result};
Expand Down Expand Up @@ -129,41 +129,27 @@ impl DocBuilder {
let gcx = compiler.gcx();
let documents = combined_sources
.par_iter()
.enumerate()
.map(|(i, (path, from_library))| {
.map(|(path, from_library)| {
let path = *path;
let from_library = *from_library;
let mut files = vec![];

// Read and parse source file
if let Some((_, ast)) = gcx.get_ast_source(path)
&& let Some(source) =
forge_fmt::format_ast(gcx, ast, self.fmt.clone().into())
if let Some((_, ast_source)) = gcx.get_ast_source(path)
&& let Some(source_unit) = ast_source.ast.as_ref()
{
let (mut source_unit, comments) = match solang_parser::parse(&source, i) {
Ok(res) => res,
Err(err) => {
if from_library {
// Ignore failures for library files
return Ok(files);
}
return Err(eyre::eyre!(
"Failed to parse Solidity code for {}\nDebug info: {:?}",
path.display(),
err
));
}
};
// Solar uses a global SourceMap: span BytePos values are global
// offsets, not per-file offsets. Subtract file.start_pos so that
// span-based indexing into the per-file source string is correct.
let source = ast_source.file.src.to_string();
let file_start = ast_source.file.start_pos.to_usize();

// Visit the parse tree
let mut doc = Parser::new(comments, source, self.fmt.tab_width);
source_unit
.visit(&mut doc)
.map_err(|err| eyre::eyre!("Failed to parse source: {err}"))?;
// Walk the solar AST directly
let doc = Parser::new(source, file_start, self.fmt.tab_width);
let all_items = doc.parse(source_unit);

// Split the parsed items on top-level constants and rest.
let (items, consts): (Vec<ParseItem>, Vec<ParseItem>) = doc
.items()
let (items, consts): (Vec<ParseItem>, Vec<ParseItem>) = all_items
.into_iter()
.partition(|item| !matches!(item.source, ParseSource::Variable(_)));

Expand Down
91 changes: 0 additions & 91 deletions crates/doc/src/helpers.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,5 @@
use itertools::Itertools;
use solang_parser::pt::FunctionDefinition;
use toml::{Value, value::Table};

/// Generates a function signature with parameter types (e.g., "functionName(type1,type2)").
/// Returns the function name without parameters if the function has no parameters.
pub fn function_signature(func: &FunctionDefinition) -> String {
let func_name = func.name.as_ref().map_or(func.ty.to_string(), |n| n.name.clone());
if func.params.is_empty() {
return func_name;
}

format!(
"{}({})",
func_name,
func.params
.iter()
.map(|p| p.1.as_ref().map(|p| p.ty.to_string()).unwrap_or_default())
.join(",")
)
}

/// 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 {
Expand All @@ -46,74 +26,3 @@ pub(crate) fn merge_toml_table(table: &mut Table, override_table: Table) {
};
}
}

#[cfg(test)]
mod tests {
use super::*;
use solang_parser::{
parse,
pt::{ContractPart, SourceUnit, SourceUnitPart},
};

#[test]
fn test_function_signature_no_params() {
let (source_unit, _) = parse(
r#"
contract Test {
function foo() public {}
}
"#,
0,
)
.unwrap();

let func = extract_function(&source_unit);
assert_eq!(function_signature(func), "foo");
}

#[test]
fn test_function_signature_with_params() {
let (source_unit, _) = parse(
r#"
contract Test {
function transfer(address to, uint256 amount) public {}
}
"#,
0,
)
.unwrap();

let func = extract_function(&source_unit);
assert_eq!(function_signature(func), "transfer(address,uint256)");
}

#[test]
fn test_function_signature_constructor() {
let (source_unit, _) = parse(
r#"
contract Test {
constructor(address owner) {}
}
"#,
0,
)
.unwrap();

let func = extract_function(&source_unit);
assert_eq!(function_signature(func), "constructor(address)");
}

/// Helper to extract the first function from a parsed source unit
fn extract_function(source_unit: &SourceUnit) -> &FunctionDefinition {
for part in &source_unit.0 {
if let SourceUnitPart::ContractDefinition(contract) = part {
for part in &contract.parts {
if let ContractPart::FunctionDefinition(func) = part {
return func;
}
}
}
}
panic!("No function found in source unit");
}
}
7 changes: 4 additions & 3 deletions crates/doc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ mod helpers;
mod parser;
pub use parser::{
Comment, CommentTag, Comments, CommentsRef, ParseItem, ParseSource, Parser, error,
source::{
BaseInfo, ContractKind, ContractSource, EnumSource, ErrorSource, EventSource,
FunctionSource, ParamInfo, StructSource, TypeSource, VariableAttr, VariableSource,
},
};

mod preprocessor;
Expand All @@ -31,6 +35,3 @@ mod writer;
pub use writer::{AsDoc, AsDocResult, BufWriter, Markdown};

pub use mdbook_driver;

// old formatter dependencies
pub mod solang_ext;
68 changes: 61 additions & 7 deletions crates/doc/src/parser/comment.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use alloy_primitives::map::HashMap;
use derive_more::{Deref, DerefMut, derive::Display};
use solang_parser::doccomment::DocCommentTag;

/// The natspec comment tag explaining the purpose of the comment.
/// See: <https://docs.soliditylang.org/en/v0.8.17/natspec-format.html#tags>.
Expand Down Expand Up @@ -70,10 +69,10 @@ impl Comment {
Self { tag, value }
}

/// Create new instance of [Comment] from [DocCommentTag]
/// Create new instance of [Comment] from a tag string and value,
/// if it has a valid natspec tag.
pub fn from_doc_comment(value: DocCommentTag) -> Option<Self> {
CommentTag::from_str(&value.tag).map(|tag| Self { tag, value: value.value })
pub fn from_tag_and_value(tag: &str, value: String) -> Option<Self> {
CommentTag::from_str(tag).map(|tag| Self { tag, value })
}

/// Split the comment at first word.
Expand Down Expand Up @@ -145,9 +144,64 @@ impl Comments {
}
}

impl From<Vec<DocCommentTag>> for Comments {
fn from(value: Vec<DocCommentTag>) -> Self {
Self(value.into_iter().filter_map(Comment::from_doc_comment).collect())
impl Comments {
/// Parse natspec comments from raw doc comment lines.
///
/// Each line should be the raw text content of a `///` or `/** */` doc comment
/// with the comment delimiters already stripped (as provided by solar's `DocComment::symbol`).
///
/// Natspec tags start with `@` (e.g. `@notice`, `@dev`, `@param`).
/// Lines without a tag at the start are treated as continuations of the previous tag,
/// or as `@notice` if no previous tag exists.
pub fn from_doc_lines(lines: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
let mut comments = Vec::new();
let mut current_tag: Option<String> = None;
let mut current_value = String::new();

let flush = |tag: &Option<String>, value: &str, out: &mut Vec<Comment>| {
let value = value.trim();
if value.is_empty() && tag.is_none() {
return;
}
let tag_str = tag.as_deref().unwrap_or("notice");
// Filter out `@solidity` tags and empty tags
if tag_str.trim() == "solidity" || tag_str.trim().is_empty() {
return;
}
if let Some(c) = Comment::from_tag_and_value(tag_str, value.to_string()) {
out.push(c);
}
};

for raw_line in lines {
let raw = raw_line.as_ref();
// For block comments, process each line individually
for line in raw.lines() {
let trimmed = line.trim().trim_start_matches('*').trim();

if let Some(rest) = trimmed.strip_prefix('@') {
// Flush previous
flush(&current_tag, &current_value, &mut comments);
// Parse new tag
let (tag, value) = rest.split_once(char::is_whitespace).unwrap_or((rest, ""));
current_tag = Some(tag.to_string());
current_value = value.trim().to_string();
} else if !trimmed.is_empty() {
// Continuation of current tag
if current_value.is_empty() {
current_value = trimmed.to_string();
} else {
current_value.push('\n');
current_value.push_str(trimmed);
}
}
}
}

// Flush last
flush(&current_tag, &current_value, &mut comments);

Self(comments)
}
}

Expand Down
Loading
Loading