Repository: foundry-rs/foundry (language: rust)

Issue:
bug(`forge`): `forge coverage --report summary / lcov` doesn't mark empty try/blocks as covered
### Component

Forge

### Have you ensured that all of these are up to date?

- [x] Foundry
- [x] Foundryup

### What version of Foundry are you on?

forge Version: 1.0.0-nightly Commit SHA: 97e2ebbb7f74189b65943ae0b8537415d709608c Build Timestamp: 2025-02-19T00:21:36.666480000Z (1739924496) Build Profile: maxperf

### What version of Foundryup are you on?

foundryup: installed - chisel Version: 1.0.0-nightly Commit SHA: 97e2ebbb7f74189b65943ae0b8537415d709608c Build Timestamp: 2025-02-19T00:21:36.666480000Z (1739924496)

### What command(s) is the bug in?

`forge coverage --report summary` and `forge coverage --report lcov`

### Operating System

macOS (Apple Silicon)

### Describe the bug

Code blocks with empty try/catch blocks like `try contract.call() {} catch {}` are marked uncovered statements in `forge coverage --report summary` even when there are tests covering cases where `contract.call()` succeeds or reverts. 

But when those blocks aren't empty, e.g. `try fn.call() {emit Event()} catch {emit Event()}`, coverage reports all statements are covered as expected.

A code example can be found [here](https://github.com/garyghayrat/forge-coverage-bug). You can see the effect on the coverage summary by commenting out `try` and `catch` statements and running `forge coverage --report summary`.

The example is kind of silly, but this try/catch pattern is often used in production code like [here](https://github.com/uniswapfoundation/UniStaker/blob/887d7dc0c1db3f17227d13af4d8a791a66912d42/src/UniStaker.sol#L306).


Discussion / hints:
Thank you, able to reproduce, probably we need to change try  / catch to branches

https://github.com/foundry-rs/foundry/blob/f3130a52937039a7e82b4aef07143139c322d09f/crates/evm/coverage/src/analysis.rs#L311-L317

Fix patch:
diff --git a/crates/evm/coverage/src/analysis.rs b/crates/evm/coverage/src/analysis.rs
index 89374668031a..37bd4d381e53 100644
--- a/crates/evm/coverage/src/analysis.rs
+++ b/crates/evm/coverage/src/analysis.rs
@@ -290,8 +290,8 @@ impl<'a> ContractVisitor<'a> {
 
                 Ok(())
             }
-            // Try-catch statement. Coverage is reported for expression, for each clause and their
-            // bodies (if any).
+            // Try-catch statement. Coverage is reported as branches for catch clauses with
+            // statements.
             NodeType::TryStatement => {
                 self.visit_expression(
                     &node
@@ -299,20 +299,53 @@ impl<'a> ContractVisitor<'a> {
                         .ok_or_else(|| eyre::eyre!("try statement had no call"))?,
                 )?;
 
-                // Add coverage for each Try-catch clause.
-                for clause in node
+                let branch_id = self.branch_id;
+                self.branch_id += 1;
+
+                let mut clauses = node
                     .attribute::<Vec<Node>>("clauses")
-                    .ok_or_else(|| eyre::eyre!("try statement had no clause"))?
-                {
-                    // Add coverage for clause statement.
-                    self.push_item_kind(CoverageItemKind::Statement, &clause.src);
-                    self.visit_statement(&clause)?;
-
-                    // Add coverage for clause body only if it is not empty.
-                    if let Some(block) = clause.attribute::<Node>("block") {
-                        if has_statements(&block) {
-                            self.push_item_kind(CoverageItemKind::Statement, &block.src);
-                            self.visit_block(&block)?;
+                    .ok_or_else(|| eyre::eyre!("try statement had no clauses"))?;
+
+                let try_block = clauses
+                    .remove(0)
+                    .attribute::<Node>("block")
+                    .ok_or_else(|| eyre::eyre!("try statement had no block"))?;
+                // Add branch with path id 0 for try (first clause).
+                self.push_item_kind(
+                    CoverageItemKind::Branch { branch_id, path_id: 0, is_first_opcode: true },
+                    &ast::LowFidelitySourceLocation {
+                        start: node.src.start,
+                        length: try_block
+                            .src
+                            .length
+                            .map(|length| try_block.src.start + length - node.src.start),
+                        index: node.src.index,
+                    },
+                );
+                self.visit_block(&try_block)?;
+
+                let mut path_id = 1;
+                for clause in clauses {
+                    if let Some(catch_block) = clause.attribute::<Node>("block") {
+                        if has_statements(&catch_block) {
+                            // Add catch branch if it has statements.
+                            self.push_item_kind(
+                                CoverageItemKind::Branch {
+                                    branch_id,
+                                    path_id,
+                                    is_first_opcode: true,
+                                },
+                                &catch_block.src,
+                            );
+                            self.visit_block(&catch_block)?;
+                            // Increment path id for next branch.
+                            path_id += 1;
+                        } else if clause.attribute::<Node>("parameters").is_some() {
+                            // Add coverage for clause with parameters and empty statements.
+                            // (`catch (bytes memory reason) {}`).
+                            // Catch all clause without statements is ignored (`catch {}`).
+                            self.push_item_kind(CoverageItemKind::Statement, &clause.src);
+                            self.visit_statement(&clause)?;
                         }
                     }
                 }


Test patch:
diff --git a/crates/forge/tests/cli/coverage.rs b/crates/forge/tests/cli/coverage.rs
index d06c9e37fa9c..2557a5fe9420 100644
--- a/crates/forge/tests/cli/coverage.rs
+++ b/crates/forge/tests/cli/coverage.rs
@@ -929,6 +929,10 @@ contract Bar {
             emit Log(reason);
         } catch (bytes memory reason) {}
     }
+
+    function tryCatchAllNewContract(address _owner) public {
+        try new Foo(_owner) returns (Foo foo_) {} catch {}
+    }
 }
     "#,
     )
@@ -958,6 +962,7 @@ contract FooTest is DSTest {
     function test_happy_path_coverage() external {
         Bar bar = new Bar();
         bar.tryCatchNewContract(0x0000000000000000000000000000000000000002);
+        bar.tryCatchAllNewContract(0x0000000000000000000000000000000000000002);
         bar.tryCatchExternalCall(1);
     }
 
@@ -965,6 +970,7 @@ contract FooTest is DSTest {
         Bar bar = new Bar();
         bar.tryCatchNewContract(0x0000000000000000000000000000000000000000);
         bar.tryCatchNewContract(0x0000000000000000000000000000000000000001);
+        bar.tryCatchAllNewContract(0x0000000000000000000000000000000000000001);
         bar.tryCatchExternalCall(0);
     }
 }
@@ -978,9 +984,9 @@ contract FooTest is DSTest {
 ╭-------------+----------------+----------------+--------------+---------------╮
 | File        | % Lines        | % Statements   | % Branches   | % Funcs       |
 +==============================================================================+
-| src/Foo.sol | 75.00% (15/20) | 66.67% (14/21) | 75.00% (3/4) | 100.00% (5/5) |
+| src/Foo.sol | 77.27% (17/22) | 78.57% (11/14) | 66.67% (6/9) | 100.00% (6/6) |
 |-------------+----------------+----------------+--------------+---------------|
-| Total       | 75.00% (15/20) | 66.67% (14/21) | 75.00% (3/4) | 100.00% (5/5) |
+| Total       | 77.27% (17/22) | 78.57% (11/14) | 66.67% (6/9) | 100.00% (6/6) |
 ╰-------------+----------------+----------------+--------------+---------------╯
 
 "#]]);
@@ -991,9 +997,9 @@ contract FooTest is DSTest {
 ╭-------------+-----------------+-----------------+---------------+---------------╮
 | File        | % Lines         | % Statements    | % Branches    | % Funcs       |
 +=================================================================================+
-| src/Foo.sol | 100.00% (20/20) | 100.00% (21/21) | 100.00% (4/4) | 100.00% (5/5) |
+| src/Foo.sol | 100.00% (22/22) | 100.00% (14/14) | 100.00% (9/9) | 100.00% (6/6) |
 |-------------+-----------------+-----------------+---------------+---------------|
-| Total       | 100.00% (20/20) | 100.00% (21/21) | 100.00% (4/4) | 100.00% (5/5) |
+| Total       | 100.00% (22/22) | 100.00% (14/14) | 100.00% (9/9) | 100.00% (6/6) |
 ╰-------------+-----------------+-----------------+---------------+---------------╯
 
 "#]]);