Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(config): use solar for inline config parsing #9615

Merged
merged 12 commits into from
Jan 5, 2025
3 changes: 2 additions & 1 deletion Cargo.lock

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

3 changes: 2 additions & 1 deletion crates/config/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ alloy-chains = { workspace = true, features = ["serde"] }
alloy-primitives = { workspace = true, features = ["serde"] }
revm-primitives.workspace = true

solang-parser.workspace = true
solar-parse.workspace = true
solar-ast.workspace = true

dirs-next = "2"
dunce.workspace = true
Expand Down
180 changes: 104 additions & 76 deletions crates/config/src/inline/natspec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ use foundry_compilers::{
};
use itertools::Itertools;
use serde_json::Value;
use solang_parser::{helpers::CodeLocation, pt};
use solar_ast::{
ast::{Arena, CommentKind, Item, ItemKind},
interface::{self, Session},
};
use solar_parse::Parser;
use std::{collections::BTreeMap, path::Path};

/// Convenient struct to hold in-line per-test configurations
Expand All @@ -30,7 +34,7 @@ impl NatSpec {
let mut natspecs: Vec<Self> = vec![];

let solc = SolcParser::new();
let solang = SolangParser::new();
let solar = SolarParser::new();
for (id, artifact) in output.artifact_ids() {
let abs_path = id.source.as_path();
let path = abs_path.strip_prefix(root).unwrap_or(abs_path);
Expand All @@ -48,7 +52,7 @@ impl NatSpec {

if !used_solc_ast {
if let Ok(src) = std::fs::read_to_string(abs_path) {
solang.parse(&mut natspecs, &src, &contract, contract_name);
solar.parse(&mut natspecs, &src, &contract, contract_name);
}
}
}
Expand Down Expand Up @@ -201,11 +205,11 @@ impl SolcParser {
}
}

struct SolangParser {
struct SolarParser {
_private: (),
}

impl SolangParser {
impl SolarParser {
fn new() -> Self {
Self { _private: () }
}
Expand All @@ -222,57 +226,85 @@ impl SolangParser {
return;
}

let Ok((pt, comments)) = solang_parser::parse(src, 0) else { return };

// Collects natspects from the given range.
let mut handle_docs = |contract: &str, func: Option<&str>, start, end| {
let docs = solang_parser::doccomment::parse_doccomments(&comments, start, end);
natspecs.extend(
docs.into_iter()
.flat_map(|doc| doc.into_comments())
.filter(|doc| doc.value.contains(INLINE_CONFIG_PREFIX))
.map(|doc| NatSpec {
// not possible to obtain correct value due to solang-parser bug
// https://github.com/hyperledger/solang/issues/1658
line: "0:0:0".to_string(),
contract: contract.to_string(),
function: func.map(|f| f.to_string()),
docs: doc.value,
}),
);
let mut handle_docs = |item: &Item<'_>| {
if item.docs.is_empty() {
return;
}
let lines = item
.docs
.iter()
.filter_map(|d| {
let s = d.symbol.as_str();
if !s.contains(INLINE_CONFIG_PREFIX) {
return None
}
match d.kind {
CommentKind::Line => Some(s.trim().to_string()),
CommentKind::Block => Some(
s.lines()
.filter(|line| line.contains(INLINE_CONFIG_PREFIX))
.map(|line| line.trim_start().trim_start_matches('*').trim())
.collect::<Vec<_>>()
.join("\n"),
),
}
})
.join("\n");
if lines.is_empty() {
return;
}
let span =
item.docs.iter().map(|doc| doc.span).reduce(|a, b| a.to(b)).unwrap_or_default();
natspecs.push(NatSpec {
contract: contract_id.to_string(),
function: if let ItemKind::Function(f) = &item.kind {
Some(
f.header
.name
.map(|sym| sym.to_string())
.unwrap_or_else(|| f.kind.to_string()),
)
} else {
None
},
line: format!("{}:{}:0", span.lo().0, span.hi().0),
docs: lines,
});
};

let mut prev_item_end = 0;
for item in &pt.0 {
let pt::SourceUnitPart::ContractDefinition(c) = item else {
prev_item_end = item.loc().end();
continue
};
let Some(id) = c.name.as_ref() else {
prev_item_end = item.loc().end();
continue
};
if id.name != contract_name {
prev_item_end = item.loc().end();
continue
};

// Handle doc comments in between the previous contract and the current one.
handle_docs(contract_id, None, prev_item_end, item.loc().start());

let mut prev_end = c.loc.start();
for part in &c.parts {
let pt::ContractPart::FunctionDefinition(f) = part else { continue };
let start = f.loc.start();
// Handle doc comments in between the previous function and the current one.
if let Some(name) = &f.name {
handle_docs(contract_id, Some(name.name.as_str()), prev_end, start);
let sess = Session::builder()
.with_silent_emitter(Some("Inline config parsing failed".to_string()))
.build();
let _ = sess.enter(|| -> interface::Result<()> {
let arena = Arena::new();

let mut parser = Parser::from_source_code(
&sess,
&arena,
interface::source_map::FileName::Custom(contract_id.to_string()),
src.to_string(),
)?;

let source_unit = parser.parse_file().map_err(|e| e.emit())?;

for item in source_unit.items.iter() {
let ItemKind::Contract(c) = &item.kind else { continue };
if c.name.as_str() != contract_name {
continue;
}

// Handle contract level doc comments.
handle_docs(item);

// Handle function level doc comments.
for item in c.body.iter() {
let ItemKind::Function(_) = &item.kind else { continue };
handle_docs(item);
}
prev_end = f.loc.end();
}

prev_item_end = item.loc().end();
}
Ok(())
});
}
}

Expand Down Expand Up @@ -318,7 +350,7 @@ mod tests {
}

#[test]
fn parse_solang() {
fn parse_solar() {
let src = "
contract C { /// forge-config: default.fuzz.runs = 600

Expand All @@ -336,47 +368,46 @@ function f2() {} /** forge-config: default.fuzz.runs = 800 */ function f3() {}
}
";
let mut natspecs = vec![];
let solang = SolangParser::new();
let id = || "path.sol:C".to_string();
let default_line = || "0:0:0".to_string();
solang.parse(&mut natspecs, src, &id(), "C");
let solar_parser = SolarParser::new();
solar_parser.parse(&mut natspecs, src, &id(), "C");
assert_eq!(
natspecs,
[
// f1
NatSpec {
contract: id(),
function: Some("f1".to_string()),
line: default_line(),
line: "14:134:0".to_string(),
docs: "forge-config: default.fuzz.runs = 600\nforge-config: default.fuzz.runs = 601".to_string(),
},
// f2
NatSpec {
contract: id(),
function: Some("f2".to_string()),
line: default_line(),
line: "164:208:0".to_string(),
docs: "forge-config: default.fuzz.runs = 700".to_string(),
},
// f3
NatSpec {
contract: id(),
function: Some("f3".to_string()),
line: default_line(),
line: "226:270:0".to_string(),
docs: "forge-config: default.fuzz.runs = 800".to_string(),
},
// f4
NatSpec {
contract: id(),
function: Some("f4".to_string()),
line: default_line(),
line: "289:391:0".to_string(),
docs: "forge-config: default.fuzz.runs = 1024\nforge-config: default.fuzz.max-test-rejects = 500".to_string(),
},
]
);
}

#[test]
fn parse_solang_2() {
fn parse_solar_2() {
let src = r#"
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
Expand All @@ -394,17 +425,16 @@ contract FuzzInlineConf is DSTest {
}
"#;
let mut natspecs = vec![];
let solang = SolangParser::new();
let solar = SolarParser::new();
let id = || "inline/FuzzInlineConf.t.sol:FuzzInlineConf".to_string();
let default_line = || "0:0:0".to_string();
solang.parse(&mut natspecs, src, &id(), "FuzzInlineConf");
solar.parse(&mut natspecs, src, &id(), "FuzzInlineConf");
assert_eq!(
natspecs,
[
NatSpec {
contract: id(),
function: Some("testInlineConfFuzz".to_string()),
line: default_line(),
line: "141:255:0".to_string(),
docs: "forge-config: default.fuzz.runs = 1024\nforge-config: default.fuzz.max-test-rejects = 500".to_string(),
},
]
Expand Down Expand Up @@ -466,7 +496,7 @@ contract FuzzInlineConf is DSTest {
}

#[test]
fn parse_solang_multiple_contracts_from_same_file() {
fn parse_solar_multiple_contracts_from_same_file() {
let src = r#"
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
Expand All @@ -484,29 +514,28 @@ contract FuzzInlineConf2 is DSTest {
}
"#;
let mut natspecs = vec![];
let solang = SolangParser::new();
let solar = SolarParser::new();
let id = || "inline/FuzzInlineConf.t.sol:FuzzInlineConf".to_string();
let default_line = || "0:0:0".to_string();
solang.parse(&mut natspecs, src, &id(), "FuzzInlineConf");
solar.parse(&mut natspecs, src, &id(), "FuzzInlineConf");
assert_eq!(
natspecs,
[NatSpec {
contract: id(),
function: Some("testInlineConfFuzz1".to_string()),
line: default_line(),
line: "142:181:0".to_string(),
docs: "forge-config: default.fuzz.runs = 1".to_string(),
},]
);

let mut natspecs = vec![];
let id = || "inline/FuzzInlineConf2.t.sol:FuzzInlineConf2".to_string();
solang.parse(&mut natspecs, src, &id(), "FuzzInlineConf2");
solar.parse(&mut natspecs, src, &id(), "FuzzInlineConf2");
assert_eq!(
natspecs,
[NatSpec {
contract: id(),
function: Some("testInlineConfFuzz2".to_string()),
line: default_line(),
line: "264:303:0".to_string(),
// should not get config from previous contract
docs: "forge-config: default.fuzz.runs = 2".to_string(),
},]
Expand All @@ -529,23 +558,22 @@ contract FuzzInlineConf is DSTest {
function testInlineConfFuzz2() {}
}"#;
let mut natspecs = vec![];
let solang = SolangParser::new();
let solar = SolarParser::new();
let id = || "inline/FuzzInlineConf.t.sol:FuzzInlineConf".to_string();
let default_line = || "0:0:0".to_string();
solang.parse(&mut natspecs, src, &id(), "FuzzInlineConf");
solar.parse(&mut natspecs, src, &id(), "FuzzInlineConf");
assert_eq!(
natspecs,
[
NatSpec {
contract: id(),
function: None,
line: default_line(),
line: "101:140:0".to_string(),
docs: "forge-config: default.fuzz.runs = 1".to_string(),
},
NatSpec {
contract: id(),
function: Some("testInlineConfFuzz1".to_string()),
line: default_line(),
line: "181:220:0".to_string(),
docs: "forge-config: default.fuzz.runs = 3".to_string(),
}
]
Expand Down
2 changes: 1 addition & 1 deletion crates/forge/tests/cli/inline_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ forgetest!(invalid_profile, |prj, cmd| {
.unwrap();

cmd.arg("test").assert_failure().stderr_eq(str![[r#"
Error: Inline config error at test/inline.sol:0:0:0: invalid profile `unknown.fuzz.runs = 2`; valid profiles: default
Error: Inline config error at test/inline.sol:80:123:0: invalid profile `unknown.fuzz.runs = 2`; valid profiles: default

"#]]);
});
Expand Down
Loading