Skip to content

Commit 289de8a

Browse files
authored
feat: require coverage library path to be specified (#904)
1 parent 8396d70 commit 289de8a

File tree

8 files changed

+90
-38
lines changed

8 files changed

+90
-38
lines changed

.changeset/poor-shirts-collect.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@nomicfoundation/edr": minor
3+
---
4+
5+
Changed the instrumentation API to require a coverage library path

Cargo.lock

Lines changed: 13 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ serde = { version = "1.0.209", default-features = false }
3737
serde_json = { version = "1.0.127", default-features = false }
3838
sha2 = { version = "0.10.8", default-features = false }
3939
sha3 = { version = "0.10.8", default-features = false }
40-
semver = { version = "1.0.23", default-features = false }
40+
semver = { version = "1.0.26", default-features = false }
4141
thiserror = { version = "1.0.58", default-features = false }
4242

4343
[workspace.lints.rust]

crates/edr_instrument/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ semver.workspace = true
1010
serde = { workspace = true, features = ["derive"] }
1111
serde_json.workspace = true
1212
sha3.workspace = true
13-
slang_solidity = "0.18.3"
13+
slang_solidity = { git = "https://github.com/NomicFoundation/slang", rev = "699de7e" }
1414
thiserror.workspace = true
1515

1616
[lints]

crates/edr_instrument/src/coverage.rs

Lines changed: 57 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ use slang_solidity::{
77
parser::{ParseError, Parser as SolidityParser, ParserInitializationError},
88
};
99

10-
#[derive(Serialize)]
10+
#[derive(Debug, Serialize)]
1111
pub struct InstrumentationMetadata {
1212
pub tag: B256,
1313
pub kind: &'static str,
1414
pub start_utf16: usize,
1515
pub end_utf16: usize,
1616
}
1717

18+
#[derive(Debug)]
1819
pub struct InstrumentationResult {
1920
pub source: String,
2021
pub metadata: Vec<InstrumentationMetadata>,
@@ -44,20 +45,37 @@ fn compute_deterministic_hash_for_statement(
4445
pub enum InstrumentationError {
4546
#[error(transparent)]
4647
Initialization(#[from] ParserInitializationError),
48+
#[error("Invalid library path.")]
49+
InvalidLibraryPath {
50+
errors: Vec<ParseError>,
51+
import: String,
52+
},
4753
#[error("Failed to parse Solidity file.")]
48-
ParseError { errors: Vec<ParseError> },
54+
InvalidSourceCode { errors: Vec<ParseError> },
4955
}
5056

5157
pub fn instrument_code(
5258
source_code: &str,
5359
source_id: &str,
5460
solidity_version: Version,
61+
coverage_library_path: &str,
5562
) -> Result<InstrumentationResult, InstrumentationError> {
5663
let parser = SolidityParser::create(solidity_version)?;
57-
let parse_result = parser.parse(SolidityParser::ROOT_KIND, source_code);
58-
if !parse_result.is_valid() {
59-
return Err(InstrumentationError::ParseError {
60-
errors: parse_result.errors().clone(),
64+
let parsed_file = parser.parse_file_contents(source_code);
65+
if !parsed_file.is_valid() {
66+
return Err(InstrumentationError::InvalidSourceCode {
67+
errors: parsed_file.errors().clone(),
68+
});
69+
}
70+
71+
let coverage_library_import = format!("\nimport \"{coverage_library_path}\";");
72+
let parsed_library_import =
73+
parser.parse_nonterminal(NonterminalKind::ImportDirective, &coverage_library_import);
74+
75+
if !parsed_library_import.is_valid() {
76+
return Err(InstrumentationError::InvalidLibraryPath {
77+
errors: parsed_library_import.errors().clone(),
78+
import: coverage_library_import,
6179
});
6280
}
6381

@@ -68,7 +86,7 @@ pub fn instrument_code(
6886
let mut metadata = Vec::new();
6987

7088
let mut statement_counter: u64 = 0;
71-
let mut cursor = parse_result.create_tree_cursor();
89+
let mut cursor = parsed_file.create_tree_cursor();
7290
while cursor.go_to_next() {
7391
match cursor.node() {
7492
Node::Nonterminal(node) if node.kind == NonterminalKind::Statement => {
@@ -97,7 +115,7 @@ pub fn instrument_code(
97115
}
98116
}
99117

100-
instrumented_source.push_str("\nimport \"hardhat/coverage.sol\";");
118+
instrumented_source.push_str(&coverage_library_import);
101119

102120
Ok(InstrumentationResult {
103121
source: instrumented_source,
@@ -112,6 +130,7 @@ mod tests {
112130
use super::*;
113131

114132
const FIXTURE: &str = include_str!("../../../data/contracts/instrumentation.sol");
133+
const LIBRARY_PATH: &str = "__hardhat_coverage.sol";
115134

116135
fn assert_metadata(
117136
metadata: &InstrumentationMetadata,
@@ -141,8 +160,8 @@ mod tests {
141160
#[test]
142161
fn determinism() {
143162
let version = Version::parse("0.8.0").expect("Failed to parse version");
144-
let result =
145-
instrument_code(FIXTURE, "instrumentation.sol", version).expect("Failed to instrument");
163+
let result = instrument_code(FIXTURE, "instrumentation.sol", version, LIBRARY_PATH)
164+
.expect("Failed to instrument");
146165

147166
let tags = result
148167
.metadata
@@ -164,17 +183,38 @@ mod tests {
164183
#[test]
165184
fn import() {
166185
let version = Version::parse("0.8.0").expect("Failed to parse version");
167-
let result =
168-
instrument_code(FIXTURE, "instrumentation.sol", version).expect("Failed to instrument");
186+
let result = instrument_code(FIXTURE, "instrumentation.sol", version, LIBRARY_PATH)
187+
.expect("Failed to instrument");
169188

170-
assert!(result.source.contains("import \"hardhat/coverage.sol\";"));
189+
assert!(
190+
result
191+
.source
192+
.contains(&format!("import \"{LIBRARY_PATH}\";"))
193+
);
194+
}
195+
196+
#[test]
197+
fn invalid_import() {
198+
let version = Version::parse("0.8.0").expect("Failed to parse version");
199+
let result = instrument_code(
200+
FIXTURE,
201+
"instrumentation.sol",
202+
version,
203+
"\"path/with/quotes.sol\"",
204+
)
205+
.expect_err("Expected an error for invalid import path");
206+
207+
assert!(matches!(
208+
result,
209+
InstrumentationError::InvalidLibraryPath { .. }
210+
));
171211
}
172212

173213
#[test]
174214
fn instrumentation() {
175215
let version = Version::parse("0.8.0").expect("Failed to parse version");
176-
let result =
177-
instrument_code(FIXTURE, "instrumentation.sol", version).expect("Failed to instrument");
216+
let result = instrument_code(FIXTURE, "instrumentation.sol", version, LIBRARY_PATH)
217+
.expect("Failed to instrument");
178218

179219
assert!(result.source.contains("__HardhatCoverage.sendHit("));
180220
assert!(result.source.contains(");"));
@@ -183,8 +223,8 @@ mod tests {
183223
#[test]
184224
fn mapping() {
185225
let version = Version::parse("0.8.0").expect("Failed to parse version");
186-
let result =
187-
instrument_code(FIXTURE, "instrumentation.sol", version).expect("Failed to instrument");
226+
let result = instrument_code(FIXTURE, "instrumentation.sol", version, LIBRARY_PATH)
227+
.expect("Failed to instrument");
188228

189229
assert_eq!(result.metadata.len(), 3);
190230

crates/edr_napi/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ export interface InstrumentationMetadata {
389389
* Adds per-statement coverage instrumentation to the given Solidity source
390390
* code.
391391
*/
392-
export declare function addStatementCoverageInstrumentation(sourceCode: string, sourceId: string, solidityVersion: string): InstrumentationResult
392+
export declare function addStatementCoverageInstrumentation(sourceCode: string, sourceId: string, solidityVersion: string, coverageLibraryPath: string): InstrumentationResult
393393
/** Ethereum execution log. */
394394
export interface ExecutionLog {
395395
address: Buffer

crates/edr_napi/src/instrument.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ pub fn add_statement_coverage_instrumentation(
8383
source_code: String,
8484
source_id: String,
8585
solidity_version: String,
86+
coverage_library_path: String,
8687
) -> napi::Result<InstrumentationResult> {
8788
let solidity_version = Version::parse(&solidity_version).map_err(|error| {
8889
napi::Error::new(
@@ -91,8 +92,13 @@ pub fn add_statement_coverage_instrumentation(
9192
)
9293
})?;
9394

94-
let instrumented = coverage::instrument_code(&source_code, &source_id, solidity_version)
95-
.map_err(|error| napi::Error::new(napi::Status::GenericFailure, error.to_string()))?;
95+
let instrumented = coverage::instrument_code(
96+
&source_code,
97+
&source_id,
98+
solidity_version,
99+
&coverage_library_path,
100+
)
101+
.map_err(|error| napi::Error::new(napi::Status::GenericFailure, error))?;
96102

97103
instrumented.try_into().map_err(|location| {
98104
napi::Error::new(

crates/edr_napi/test/instrument.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,16 @@ describe("Code coverage", () => {
2727

2828
describe("instrumentation", function () {
2929
it("Statement coverage", async function () {
30+
const coverageLibraryPath = "__hardhat_coverage.sol";
3031
const result = addStatementCoverageInstrumentation(
3132
incrementSourceCode,
3233
"instrumentation.sol",
33-
"0.8.0"
34+
"0.8.0",
35+
coverageLibraryPath
3436
);
3537

3638
expect(result.source).to.contain("__HardhatCoverage.sendHit(");
39+
expect(result.source).to.contain(coverageLibraryPath);
3740

3841
assert.lengthOf(result.metadata, 3);
3942
assertMetadata(result.metadata[0], {

0 commit comments

Comments
 (0)